diff options
-rw-r--r-- | ChangeLog | 66 | ||||
-rw-r--r-- | README | 2 | ||||
-rw-r--r-- | __pkginfo__.py | 2 | ||||
-rwxr-xr-x | bin/pytest | 2 | ||||
-rw-r--r-- | configuration.py | 118 | ||||
-rw-r--r-- | date.py | 2 | ||||
-rw-r--r-- | debian.lenny/python-logilab-common.preinst | 23 | ||||
-rw-r--r-- | debian/changelog | 18 | ||||
-rw-r--r-- | deprecation.py | 232 | ||||
-rw-r--r-- | graph.py | 4 | ||||
-rw-r--r-- | modutils.py | 21 | ||||
-rw-r--r-- | optik_ext.py | 8 | ||||
-rw-r--r-- | pdf_ext.py | 111 | ||||
-rw-r--r-- | python-logilab-common.spec | 2 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rw-r--r-- | shellutils.py | 15 | ||||
-rw-r--r-- | table.py | 2 | ||||
-rw-r--r-- | tasksqueue.py | 2 | ||||
-rw-r--r-- | test/unittest_configuration.py | 49 | ||||
-rw-r--r-- | test/unittest_date.py | 7 | ||||
-rw-r--r-- | test/unittest_deprecation.py | 61 | ||||
-rw-r--r-- | testlib.py | 6 |
22 files changed, 426 insertions, 334 deletions
@@ -1,21 +1,54 @@ ChangeLog for logilab.common ============================ --- +2014-02-11 -- 0.61.0 + * pdf_ext: removed, it had no known users (CVE-2014-1838) + + * shellutils: fix tempfile issue in Execute, and deprecate it + (CVE-2014-1839) + + * pytest: use 'env' to run the python interpreter + + * graph: ensure output is ordered on node and graph ids (#202314) + + +2013-16-12 -- 0.60.1 * modutils: - * fix typo causing name error in python3 / bad message in python2 - (#136037) + * don't propagate IOError when package's __init__.py file doesn't + exist (#174606) + + * ensure file is closed, may cause pb depending on the interpreter, eg + pypy) (#180876) + + * fix support for `extend_path` based nested namespace packages ; + Report and patch by John Johnson (#177651) + + * fix some cases of failing python3 install on windows platform / cross + compilation (#180836) + - * fix python3.3 crash in file_from_modpath due to implementation + +2013-07-26 -- 0.60.0 + * configuration: rename option_name method into option_attrname (#140667) + + * deprecation: new DeprecationManager class (closes #108205) + + * modutils: + + - fix typo causing name error in python3 / bad message in python2 + (#136037) + - fix python3.3 crash in file_from_modpath due to implementation change of imp.find_module wrt builtin modules (#137244) - * testlib: use assertCountEqual instead of assertSameElements/assertItemsEqual + * testlib: use assertCountEqual instead of assertSameElements/assertItemsEqual (deprecated), fixing crash with python 3.3 (#144526) * graph: use codecs.open avoid crash when writing utf-8 data under python3 (#155138) + + 2013-04-16 -- 0.59.1 * graph: added pruning of the recursive search tree for detecting cycles in graphs (closes #2469) @@ -24,11 +57,10 @@ ChangeLog for logilab.common * registry: - * select_or_none should not silent ObjectNotFound exception - (closes #119819) - - * remove 2 accidentally introduced tabs breaking python 3 compat - (closes #117580) + - select_or_none should not silent ObjectNotFound exception + (closes #119819) + - remove 2 accidentally introduced tabs breaking python 3 compat + (closes #117580) * fix umessages test w/ python 3 and LC_ALL=C (closes #119967, report and patch by Ian Delaney) @@ -39,15 +71,15 @@ ChangeLog for logilab.common * registry: - introduce RegistrableObject base class, mandatory to make - classes automatically registrable, and cleanup code - accordingly + classes automatically registrable, and cleanup code + accordingly - introduce objid and objname methods on Registry instead of - classid function and inlined code plus other refactorings to allow - arbitrary objects to be registered, provided they inherit from new - RegistrableInstance class (closes #98742) + classid function and inlined code plus other refactorings to allow + arbitrary objects to be registered, provided they inherit from new + RegistrableInstance class (closes #98742) - deprecate usage of leading underscore to skip object registration, using - __abstract__ explicitly is better and notion of registered object 'name' - is now somewhat fuzzy + __abstract__ explicitly is better and notion of registered object 'name' + is now somewhat fuzzy - use register_all when no registration callback defined (closes #111011) * logging_ext: on windows, use colorama to display colored logs, if available (closes #107436) @@ -123,8 +123,6 @@ Modules extending some external modules * `hg`, some Mercurial_ utility functions. -* `pdf_ext`, pdf and fdf file manipulations, with pdftk. - * `pyro_ext`, some Pyro_ utility functions. * `sphinx_ext`, Sphinx_ plugin defining a `autodocstring` directive. diff --git a/__pkginfo__.py b/__pkginfo__.py index 4d3496e..d3be555 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -25,7 +25,7 @@ modname = 'common' subpackage_of = 'logilab' subpackage_master = True -numversion = (0, 59, 1) +numversion = (0, 61, 0) version = '.'.join([str(num) for num in numversion]) license = 'LGPL' # 2.1 or later @@ -1,4 +1,4 @@ -#!/usr/bin/python -u +#!/usr/bin/env python import warnings warnings.simplefilter('default', DeprecationWarning) diff --git a/configuration.py b/configuration.py index 993f759..fa93a05 100644 --- a/configuration.py +++ b/configuration.py @@ -114,11 +114,11 @@ from ConfigParser import ConfigParser, NoOptionError, NoSectionError, \ from warnings import warn from logilab.common.compat import callable, raw_input, str_encode as _encode - +from logilab.common.deprecation import deprecated from logilab.common.textutils import normalize_text, unquote -from logilab.common import optik_ext as optparse +from logilab.common import optik_ext -OptionError = optparse.OptionError +OptionError = optik_ext.OptionError REQUIRED = [] @@ -136,63 +136,66 @@ def _get_encoding(encoding, stream): # validation functions ######################################################## +# validators will return the validated value or raise optparse.OptionValueError +# XXX add to documentation + def choice_validator(optdict, name, value): """validate and return a converted value for option of type 'choice' """ if not value in optdict['choices']: msg = "option %s: invalid value: %r, should be in %s" - raise optparse.OptionValueError(msg % (name, value, optdict['choices'])) + raise optik_ext.OptionValueError(msg % (name, value, optdict['choices'])) return value def multiple_choice_validator(optdict, name, value): """validate and return a converted value for option of type 'choice' """ choices = optdict['choices'] - values = optparse.check_csv(None, name, value) + values = optik_ext.check_csv(None, name, value) for value in values: if not value in choices: msg = "option %s: invalid value: %r, should be in %s" - raise optparse.OptionValueError(msg % (name, value, choices)) + raise optik_ext.OptionValueError(msg % (name, value, choices)) return values def csv_validator(optdict, name, value): """validate and return a converted value for option of type 'csv' """ - return optparse.check_csv(None, name, value) + return optik_ext.check_csv(None, name, value) def yn_validator(optdict, name, value): """validate and return a converted value for option of type 'yn' """ - return optparse.check_yn(None, name, value) + return optik_ext.check_yn(None, name, value) def named_validator(optdict, name, value): """validate and return a converted value for option of type 'named' """ - return optparse.check_named(None, name, value) + return optik_ext.check_named(None, name, value) def file_validator(optdict, name, value): """validate and return a filepath for option of type 'file'""" - return optparse.check_file(None, name, value) + return optik_ext.check_file(None, name, value) def color_validator(optdict, name, value): """validate and return a valid color for option of type 'color'""" - return optparse.check_color(None, name, value) + return optik_ext.check_color(None, name, value) def password_validator(optdict, name, value): """validate and return a string for option of type 'password'""" - return optparse.check_password(None, name, value) + return optik_ext.check_password(None, name, value) def date_validator(optdict, name, value): """validate and return a mx DateTime object for option of type 'date'""" - return optparse.check_date(None, name, value) + return optik_ext.check_date(None, name, value) def time_validator(optdict, name, value): """validate and return a time object for option of type 'time'""" - return optparse.check_time(None, name, value) + return optik_ext.check_time(None, name, value) def bytes_validator(optdict, name, value): """validate and return an integer for option of type 'bytes'""" - return optparse.check_bytes(None, name, value) + return optik_ext.check_bytes(None, name, value) VALIDATORS = {'string': unquote, @@ -222,14 +225,18 @@ def _call_validator(opttype, optdict, option, value): except TypeError: try: return VALIDATORS[opttype](value) - except optparse.OptionValueError: + except optik_ext.OptionValueError: raise except: - raise optparse.OptionValueError('%s value (%r) should be of type %s' % + raise optik_ext.OptionValueError('%s value (%r) should be of type %s' % (option, value, opttype)) # user input functions ######################################################## +# user input functions will ask the user for input on stdin then validate +# the result and return the validated value or raise optparse.OptionValueError +# XXX add to documentation + def input_password(optdict, question='password:'): from getpass import getpass while True: @@ -251,7 +258,7 @@ def _make_input_function(opttype): return None try: return _call_validator(opttype, optdict, None, value) - except optparse.OptionValueError, ex: + except optik_ext.OptionValueError, ex: msg = str(ex).split(':', 1)[-1].strip() print 'bad value: %s' % msg return input_validator @@ -264,6 +271,8 @@ INPUT_FUNCTIONS = { for opttype in VALIDATORS.keys(): INPUT_FUNCTIONS.setdefault(opttype, _make_input_function(opttype)) +# utility functions ############################################################ + def expand_default(self, option): """monkey patch OptionParser.expand_default since we have a particular way to handle defaults to avoid overriding values in the configuration @@ -278,15 +287,15 @@ def expand_default(self, option): value = None else: optdict = provider.get_option_def(optname) - optname = provider.option_name(optname, optdict) + optname = provider.option_attrname(optname, optdict) value = getattr(provider.config, optname, optdict) value = format_option_value(optdict, value) - if value is optparse.NO_DEFAULT or not value: + if value is optik_ext.NO_DEFAULT or not value: value = self.NO_DEFAULT_VALUE return option.help.replace(self.default_tag, str(value)) -def convert(value, optdict, name=''): +def _validate(value, optdict, name=''): """return a validated value for an option according to its type optional argument name is only used for error message formatting @@ -297,6 +306,9 @@ def convert(value, optdict, name=''): # FIXME return value return _call_validator(_type, optdict, name, value) +convert = deprecated('[0.60] convert() was renamed _validate()')(_validate) + +# format and output functions ################################################## def comment(string): """return string as a comment""" @@ -401,6 +413,7 @@ def rest_format_section(stream, section, options, encoding=None, doc=None): print >> stream, '' print >> stream, ' Default: ``%s``' % value.replace("`` ", "```` ``") +# Options Manager ############################################################## class OptionsManagerMixIn(object): """MixIn to handle a configuration from both a configuration file and @@ -425,7 +438,7 @@ class OptionsManagerMixIn(object): # configuration file parser self.cfgfile_parser = ConfigParser() # command line parser - self.cmdline_parser = optparse.OptionParser(usage=usage, version=version) + self.cmdline_parser = optik_ext.OptionParser(usage=usage, version=version) self.cmdline_parser.options_manager = self self._optik_option_attrs = set(self.cmdline_parser.option_class.ATTRS) @@ -461,7 +474,7 @@ class OptionsManagerMixIn(object): if group_name in self._mygroups: group = self._mygroups[group_name] else: - group = optparse.OptionGroup(self.cmdline_parser, + group = optik_ext.OptionGroup(self.cmdline_parser, title=group_name.capitalize()) self.cmdline_parser.add_option_group(group) group.level = provider.level @@ -497,9 +510,9 @@ class OptionsManagerMixIn(object): # default is handled here and *must not* be given to optik if you # want the whole machinery to work if 'default' in optdict: - if (optparse.OPTPARSE_FORMAT_DEFAULT and 'help' in optdict and - optdict.get('default') is not None and - not optdict['action'] in ('store_true', 'store_false')): + if ('help' in optdict + and optdict.get('default') is not None + and not optdict['action'] in ('store_true', 'store_false')): optdict['help'] += ' [current: %default]' del optdict['default'] args = ['--' + str(opt)] @@ -566,7 +579,7 @@ class OptionsManagerMixIn(object): """ self._monkeypatch_expand_default() try: - optparse.generate_manpage(self.cmdline_parser, pkginfo, + optik_ext.generate_manpage(self.cmdline_parser, pkginfo, section, stream=stream or sys.stdout, level=self._maxlevel) finally: @@ -686,7 +699,7 @@ class OptionsManagerMixIn(object): def add_help_section(self, title, description, level=0): """add a dummy option section for help purpose """ - group = optparse.OptionGroup(self.cmdline_parser, + group = optik_ext.OptionGroup(self.cmdline_parser, title=title.capitalize(), description=description) group.level = level @@ -694,18 +707,18 @@ class OptionsManagerMixIn(object): self.cmdline_parser.add_option_group(group) def _monkeypatch_expand_default(self): - # monkey patch optparse to deal with our default values + # monkey patch optik_ext to deal with our default values try: - self.__expand_default_backup = optparse.HelpFormatter.expand_default - optparse.HelpFormatter.expand_default = expand_default + self.__expand_default_backup = optik_ext.HelpFormatter.expand_default + optik_ext.HelpFormatter.expand_default = expand_default except AttributeError: # python < 2.4: nothing to be done pass def _unmonkeypatch_expand_default(self): # remove monkey patch - if hasattr(optparse.HelpFormatter, 'expand_default'): - # unpatch optparse to avoid side effects - optparse.HelpFormatter.expand_default = self.__expand_default_backup + if hasattr(optik_ext.HelpFormatter, 'expand_default'): + # unpatch optik_ext to avoid side effects + optik_ext.HelpFormatter.expand_default = self.__expand_default_backup def help(self, level=0): """return the usage string for available options """ @@ -734,6 +747,7 @@ class Method(object): assert self._inst, 'unbound method' return getattr(self._inst, self.method)(*args, **kwargs) +# Options Provider ############################################################# class OptionsProviderMixIn(object): """Mixin to provide options to an OptionsManager""" @@ -745,7 +759,7 @@ class OptionsProviderMixIn(object): level = 0 def __init__(self): - self.config = optparse.Values() + self.config = optik_ext.Values() for option in self.options: try: option, optdict = option @@ -777,41 +791,41 @@ class OptionsProviderMixIn(object): default = default() return default - def option_name(self, opt, optdict=None): + def option_attrname(self, opt, optdict=None): """get the config attribute corresponding to opt """ if optdict is None: optdict = self.get_option_def(opt) return optdict.get('dest', opt.replace('-', '_')) + option_name = deprecated('[0.60] OptionsProviderMixIn.option_name() was renamed to option_attrname()')(option_attrname) def option_value(self, opt): """get the current value for the given option""" - return getattr(self.config, self.option_name(opt), None) + return getattr(self.config, self.option_attrname(opt), None) def set_option(self, opt, value, action=None, optdict=None): """method called to set an option (registered in the options list) """ - # print "************ setting option", opt," to value", value if optdict is None: optdict = self.get_option_def(opt) if value is not None: - value = convert(value, optdict, opt) + value = _validate(value, optdict, opt) if action is None: action = optdict.get('action', 'store') if optdict.get('type') == 'named': # XXX need specific handling - optname = self.option_name(opt, optdict) + optname = self.option_attrname(opt, optdict) currentvalue = getattr(self.config, optname, None) if currentvalue: currentvalue.update(value) value = currentvalue if action == 'store': - setattr(self.config, self.option_name(opt, optdict), value) + setattr(self.config, self.option_attrname(opt, optdict), value) elif action in ('store_true', 'count'): - setattr(self.config, self.option_name(opt, optdict), 0) + setattr(self.config, self.option_attrname(opt, optdict), 0) elif action == 'store_false': - setattr(self.config, self.option_name(opt, optdict), 1) + setattr(self.config, self.option_attrname(opt, optdict), 1) elif action == 'append': - opt = self.option_name(opt, optdict) + opt = self.option_attrname(opt, optdict) _list = getattr(self.config, opt, None) if _list is None: if isinstance(value, (list, tuple)): @@ -893,6 +907,7 @@ class OptionsProviderMixIn(object): for optname, optdict in options: yield (optname, optdict, self.option_value(optname)) +# configuration ################################################################ class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): """basic mixin for simple configurations which don't need the @@ -913,7 +928,7 @@ class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): continue if not gdef in self.option_groups: self.option_groups.append(gdef) - self.register_options_provider(self, own_group=0) + self.register_options_provider(self, own_group=False) def register_options(self, options): """add some options to the configuration""" @@ -932,8 +947,8 @@ class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): def __getitem__(self, key): try: - return getattr(self.config, self.option_name(key)) - except (optparse.OptionValueError, AttributeError): + return getattr(self.config, self.option_attrname(key)) + except (optik_ext.OptionValueError, AttributeError): raise KeyError(key) def __setitem__(self, key, value): @@ -941,7 +956,7 @@ class ConfigurationMixIn(OptionsManagerMixIn, OptionsProviderMixIn): def get(self, key, default=None): try: - return getattr(self.config, self.option_name(key)) + return getattr(self.config, self.option_attrname(key)) except (OptionError, AttributeError): return default @@ -977,20 +992,21 @@ class OptionsManager2ConfigurationAdapter(object): def __getitem__(self, key): provider = self.config._all_options[key] try: - return getattr(provider.config, provider.option_name(key)) + return getattr(provider.config, provider.option_attrname(key)) except AttributeError: raise KeyError(key) def __setitem__(self, key, value): - self.config.global_set_option(self.config.option_name(key), value) + self.config.global_set_option(self.config.option_attrname(key), value) def get(self, key, default=None): provider = self.config._all_options[key] try: - return getattr(provider.config, provider.option_name(key)) + return getattr(provider.config, provider.option_attrname(key)) except AttributeError: return default +# other functions ############################################################## def read_old_config(newconfig, changes, configfile): """initialize newconfig from a deprecated configuration file @@ -188,8 +188,8 @@ def date_range(begin, end, incday=None, incmonth=None): end = todate(end) if incmonth: while begin < end: - begin = next_month(begin, incmonth) yield begin + begin = next_month(begin, incmonth) else: incr = get_step(begin, incday or 1) while begin < end: diff --git a/debian.lenny/python-logilab-common.preinst b/debian.lenny/python-logilab-common.preinst deleted file mode 100644 index 9c71641..0000000 --- a/debian.lenny/python-logilab-common.preinst +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -e - -case "$1" in - install) - ;; - upgrade) - pycentral pkgremove python-logilab-common 2>/dev/null || true - rm -vrf /usr/lib/$(pyversions -d)/site-packages/logilab/common - if [[ $(find /usr/lib/$(pyversions -d)/site-packages/logilab/ -maxdepth 1 -type d | wc -l) = '1' ]]; then - rm -vrf /usr/lib/$(pyversions -d)/site-packages/logilab/ - fi - ;; - abort-upgrade) - ;; - *) - echo "preinst called with unknown argument '$1'" >&2 - exit 1 - ;; -esac - -#DEBHELPER# - -exit 0 diff --git a/debian/changelog b/debian/changelog index 2c94e9f..49b1834 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +logilab-common (0.61.0-1) unstable; urgency=low + + * new upstream release + + -- Julien Cristau <julien.cristau@logilab.fr> Tue, 11 Feb 2014 15:37:02 +0100 + +logilab-common (0.60.1-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Mon, 16 Dec 2013 12:15:51 +0100 + +logilab-common (0.60.0-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Fri, 26 Jul 2013 10:30:04 +0200 + logilab-common (0.59.1-1) unstable; urgency=low * new upstream release diff --git a/deprecation.py b/deprecation.py index 5e2f813..02e4edb 100644 --- a/deprecation.py +++ b/deprecation.py @@ -22,93 +22,7 @@ __docformat__ = "restructuredtext en" import sys from warnings import warn -class class_deprecated(type): - """metaclass to print a warning on instantiation of a deprecated class""" - - def __call__(cls, *args, **kwargs): - msg = getattr(cls, "__deprecation_warning__", - "%(cls)s is deprecated") % {'cls': cls.__name__} - warn(msg, DeprecationWarning, stacklevel=2) - return type.__call__(cls, *args, **kwargs) - - -def class_renamed(old_name, new_class, message=None): - """automatically creates a class which fires a DeprecationWarning - when instantiated. - - >>> Set = class_renamed('Set', set, 'Set is now replaced by set') - >>> s = Set() - sample.py:57: DeprecationWarning: Set is now replaced by set - s = Set() - >>> - """ - clsdict = {} - if message is None: - message = '%s is deprecated, use %s' % (old_name, new_class.__name__) - clsdict['__deprecation_warning__'] = message - try: - # new-style class - return class_deprecated(old_name, (new_class,), clsdict) - except (NameError, TypeError): - # old-style class - class DeprecatedClass(new_class): - """FIXME: There might be a better way to handle old/new-style class - """ - def __init__(self, *args, **kwargs): - warn(message, DeprecationWarning, stacklevel=2) - new_class.__init__(self, *args, **kwargs) - return DeprecatedClass - - -def class_moved(new_class, old_name=None, message=None): - """nice wrapper around class_renamed when a class has been moved into - another module - """ - if old_name is None: - old_name = new_class.__name__ - if message is None: - message = 'class %s is now available as %s.%s' % ( - old_name, new_class.__module__, new_class.__name__) - return class_renamed(old_name, new_class, message) - -def deprecated(reason=None, stacklevel=2, name=None, doc=None): - """Decorator that raises a DeprecationWarning to print a message - when the decorated function is called. - """ - def deprecated_decorator(func): - message = reason or 'The function "%s" is deprecated' - if '%s' in message: - message = message % func.func_name - def wrapped(*args, **kwargs): - warn(message, DeprecationWarning, stacklevel=stacklevel) - return func(*args, **kwargs) - try: - wrapped.__name__ = name or func.__name__ - except TypeError: # readonly attribute in 2.3 - pass - wrapped.__doc__ = doc or func.__doc__ - return wrapped - return deprecated_decorator - -def moved(modpath, objname): - """use to tell that a callable has been moved to a new module. - - It returns a callable wrapper, so that when its called a warning is printed - telling where the object can be found, import is done (and not before) and - the actual object is called. - - NOTE: the usage is somewhat limited on classes since it will fail if the - wrapper is use in a class ancestors list, use the `class_moved` function - instead (which has no lazy import feature though). - """ - def callnew(*args, **kwargs): - from logilab.common.modutils import load_module_from_name - message = "object %s has been moved to module %s" % (objname, modpath) - warn(message, DeprecationWarning, stacklevel=2) - m = load_module_from_name(modpath) - return getattr(m, objname)(*args, **kwargs) - return callnew - +from logilab.common.changelog import Version class DeprecationWrapper(object): @@ -128,3 +42,147 @@ class DeprecationWrapper(object): else: warn(self._msg, DeprecationWarning, stacklevel=2) setattr(self._proxied, attr, value) + + +class DeprecationManager(object): + """Manage the deprecation message handling. Messages are dropped for + versions more recent than the 'compatible' version. Example:: + + deprecator = deprecation.DeprecationManager("module_name") + deprecator.compatibility('1.3') + + deprecator.warn('1.2', "message.") + + @deprecator.deprecated('1.2', 'Message') + def any_func(): + pass + + class AnyClass(object): + __metaclass__ = deprecator.class_deprecated('1.2') + """ + def __init__(self, module_name=None): + """ + """ + self.module_name = module_name + self.compatible_version = None + + def compatibility(self, compatible_version): + """Set the compatible version. + """ + self.compatible_version = Version(compatible_version) + + def deprecated(self, version=None, reason=None, stacklevel=2, name=None, doc=None): + """Display a deprecation message only if the version is older than the + compatible version. + """ + def decorator(func): + message = reason or 'The function "%s" is deprecated' + if '%s' in message: + message %= func.func_name + def wrapped(*args, **kwargs): + self.warn(version, message, stacklevel+1) + return func(*args, **kwargs) + return wrapped + return decorator + + def class_deprecated(self, version=None): + class metaclass(type): + """metaclass to print a warning on instantiation of a deprecated class""" + + def __call__(cls, *args, **kwargs): + msg = getattr(cls, "__deprecation_warning__", + "%(cls)s is deprecated") % {'cls': cls.__name__} + self.warn(version, msg, stacklevel=3) + return type.__call__(cls, *args, **kwargs) + return metaclass + + def moved(self, version, modpath, objname): + """use to tell that a callable has been moved to a new module. + + It returns a callable wrapper, so that when its called a warning is printed + telling where the object can be found, import is done (and not before) and + the actual object is called. + + NOTE: the usage is somewhat limited on classes since it will fail if the + wrapper is use in a class ancestors list, use the `class_moved` function + instead (which has no lazy import feature though). + """ + def callnew(*args, **kwargs): + from logilab.common.modutils import load_module_from_name + message = "object %s has been moved to module %s" % (objname, modpath) + self.warn(version, message) + m = load_module_from_name(modpath) + return getattr(m, objname)(*args, **kwargs) + return callnew + + def class_renamed(self, version, old_name, new_class, message=None): + clsdict = {} + if message is None: + message = '%s is deprecated, use %s' % (old_name, new_class.__name__) + clsdict['__deprecation_warning__'] = message + try: + # new-style class + return self.class_deprecated(version)(old_name, (new_class,), clsdict) + except (NameError, TypeError): + # old-style class + class DeprecatedClass(new_class): + """FIXME: There might be a better way to handle old/new-style class + """ + def __init__(self, *args, **kwargs): + self.warn(version, message, stacklevel=3) + new_class.__init__(self, *args, **kwargs) + return DeprecatedClass + + def class_moved(self, version, new_class, old_name=None, message=None): + """nice wrapper around class_renamed when a class has been moved into + another module + """ + if old_name is None: + old_name = new_class.__name__ + if message is None: + message = 'class %s is now available as %s.%s' % ( + old_name, new_class.__module__, new_class.__name__) + return self.class_renamed(version, old_name, new_class, message) + + def warn(self, version=None, reason="", stacklevel=2): + """Display a deprecation message only if the version is older than the + compatible version. + """ + if (self.compatible_version is None + or version is None + or Version(version) < self.compatible_version): + if self.module_name and version: + reason = '[%s %s] %s' % (self.module_name, version, reason) + elif self.module_name: + reason = '[%s] %s' % (self.module_name, reason) + elif version: + reason = '[%s] %s' % (version, reason) + warn(reason, DeprecationWarning, stacklevel=stacklevel) + +_defaultdeprecator = DeprecationManager() + +def deprecated(reason=None, stacklevel=2, name=None, doc=None): + return _defaultdeprecator.deprecated(None, reason, stacklevel, name, doc) + +class_deprecated = _defaultdeprecator.class_deprecated() + +def moved(modpath, objname): + return _defaultdeprecator.moved(None, modpath, objname) +moved.__doc__ = _defaultdeprecator.moved.__doc__ + +def class_renamed(old_name, new_class, message=None): + """automatically creates a class which fires a DeprecationWarning + when instantiated. + + >>> Set = class_renamed('Set', set, 'Set is now replaced by set') + >>> s = Set() + sample.py:57: DeprecationWarning: Set is now replaced by set + s = Set() + >>> + """ + return _defaultdeprecator.class_renamed(None, old_name, new_class, message) + +def class_moved(new_class, old_name=None, message=None): + return _defaultdeprecator.class_moved(None, new_class, old_name, message) +class_moved.__doc__ = _defaultdeprecator.class_moved.__doc__ + @@ -134,14 +134,14 @@ class DotBackend: """ attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()] n_from, n_to = normalize_node_id(name1), normalize_node_id(name2) - self.emit('%s -> %s [%s];' % (n_from, n_to, ", ".join(attrs)) ) + self.emit('%s -> %s [%s];' % (n_from, n_to, ', '.join(sorted(attrs))) ) def emit_node(self, name, **props): """emit a node with given properties. node properties: see http://www.graphviz.org/doc/info/attrs.html """ attrs = ['%s="%s"' % (prop, value) for prop, value in props.items()] - self.emit('%s [%s];' % (normalize_node_id(name), ", ".join(attrs))) + self.emit('%s [%s];' % (normalize_node_id(name), ', '.join(sorted(attrs)))) def normalize_node_id(nid): """Returns a suitable DOT node id for `nid`.""" diff --git a/modutils.py b/modutils.py index 9d0bb49..2756841 100644 --- a/modutils.py +++ b/modutils.py @@ -27,6 +27,8 @@ :type BUILTIN_MODULES: dict :var BUILTIN_MODULES: dictionary with builtin module names has key """ +from __future__ import with_statement + __docformat__ = "restructuredtext en" import sys @@ -656,14 +658,19 @@ def _module_file(modpath, path=None): '.'.join(imported))) # XXX guess if package is using pkgutil.extend_path by looking for # those keywords in the first four Kbytes - data = open(join(mp_filename, '__init__.py')).read(4096) - if 'pkgutil' in data and 'extend_path' in data: - # extend_path is called, search sys.path for module/packages of this name - # see pkgutil.extend_path documentation - path = [join(p, modname) for p in sys.path - if isdir(join(p, modname))] - else: + try: + with open(join(mp_filename, '__init__.py')) as stream: + data = stream.read(4096) + except IOError: path = [mp_filename] + else: + if 'pkgutil' in data and 'extend_path' in data: + # extend_path is called, search sys.path for module/packages + # of this name see pkgutil.extend_path documentation + path = [join(p, *imported) for p in sys.path + if isdir(join(p, *imported))] + else: + path = [mp_filename] return mtype, mp_filename def _is_python_file(filename): diff --git a/optik_ext.py b/optik_ext.py index 39bbe18..49d685b 100644 --- a/optik_ext.py +++ b/optik_ext.py @@ -65,9 +65,6 @@ try: except ImportError: HAS_MX_DATETIME = False - -OPTPARSE_FORMAT_DEFAULT = sys.version_info >= (2, 4) - from logilab.common.textutils import splitstrip def check_regexp(option, opt, value): @@ -227,10 +224,7 @@ class Option(BaseOption): def process(self, opt, value, values, parser): # First, convert the value(s) to the right type. Howl if any # value(s) are bogus. - try: - value = self.convert_value(opt, value) - except AttributeError: # py < 2.4 - value = self.check_value(opt, value) + value = self.convert_value(opt, value) if self.type == 'named': existant = getattr(values, self.dest) if existant: diff --git a/pdf_ext.py b/pdf_ext.py deleted file mode 100644 index 71c483b..0000000 --- a/pdf_ext.py +++ /dev/null @@ -1,111 +0,0 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of logilab-common. -# -# logilab-common is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) any -# later version. -# -# logilab-common is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with logilab-common. If not, see <http://www.gnu.org/licenses/>. -"""Manipulate pdf and fdf files (pdftk recommended). - -Notes regarding pdftk, pdf forms and fdf files (form definition file) -fields names can be extracted with: - - pdftk orig.pdf generate_fdf output truc.fdf - -to merge fdf and pdf: - - pdftk orig.pdf fill_form test.fdf output result.pdf [flatten] - -without flatten, one could further edit the resulting form. -with flatten, everything is turned into text. - - - - -""" -__docformat__ = "restructuredtext en" -# XXX seems very unix specific -# TODO: check availability of pdftk at import - - -import os - -HEAD="""%FDF-1.2 -%\xE2\xE3\xCF\xD3 -1 0 obj -<< -/FDF -<< -/Fields [ -""" - -TAIL="""] ->> ->> -endobj -trailer - -<< -/Root 1 0 R ->> -%%EOF -""" - -def output_field( f ): - return "\xfe\xff" + "".join( [ "\x00"+c for c in f ] ) - -def extract_keys(lines): - keys = [] - for line in lines: - if line.startswith('/V'): - pass #print 'value',line - elif line.startswith('/T'): - key = line[7:-2] - key = ''.join(key.split('\x00')) - keys.append( key ) - return keys - -def write_field(out, key, value): - out.write("<<\n") - if value: - out.write("/V (%s)\n" %value) - else: - out.write("/V /\n") - out.write("/T (%s)\n" % output_field(key) ) - out.write(">> \n") - -def write_fields(out, fields): - out.write(HEAD) - for (key, value, comment) in fields: - write_field(out, key, value) - write_field(out, key+"a", value) # pour copie-carbone sur autres pages - out.write(TAIL) - -def extract_keys_from_pdf(filename): - # what about using 'pdftk filename dump_data_fields' and parsing the output ? - os.system('pdftk %s generate_fdf output /tmp/toto.fdf' % filename) - lines = file('/tmp/toto.fdf').readlines() - return extract_keys(lines) - - -def fill_pdf(infile, outfile, fields): - write_fields(file('/tmp/toto.fdf', 'w'), fields) - os.system('pdftk %s fill_form /tmp/toto.fdf output %s flatten' % (infile, outfile)) - -def testfill_pdf(infile, outfile): - keys = extract_keys_from_pdf(infile) - fields = [] - for key in keys: - fields.append( (key, key, '') ) - fill_pdf(infile, outfile, fields) - diff --git a/python-logilab-common.spec b/python-logilab-common.spec index 0544013..f63e49c 100644 --- a/python-logilab-common.spec +++ b/python-logilab-common.spec @@ -10,7 +10,7 @@ %{!?_python_sitelib: %define _python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} Name: %{python}-logilab-common -Version: 0.59.1 +Version: 0.61.0 Release: logilab.1%{?dist} Summary: Common libraries for Logilab projects @@ -139,11 +139,12 @@ class MyBuildPy(build_py): shutil.copytree(directory, dest) if sys.version_info >= (3, 0): # process manually python file in include_dirs (test data) - from subprocess import check_call + from distutils.util import run_2to3 # brackets are NOT optional here for py3k compat print('running 2to3 on', dest) - # Needs `shell=True` to run on Windows. - check_call(['2to3', '-wn', dest], shell=True) + run_2to3([dest]) + + def install(**kwargs): diff --git a/shellutils.py b/shellutils.py index 60ef602..28c2b42 100644 --- a/shellutils.py +++ b/shellutils.py @@ -31,11 +31,13 @@ import fnmatch import errno import string import random +import subprocess from os.path import exists, isdir, islink, basename, join from logilab.common import STD_BLACKLIST, _handle_blacklist from logilab.common.compat import raw_input from logilab.common.compat import str_to_bytes +from logilab.common.deprecation import deprecated try: from logilab.common.proc import ProcInfo, NoSuchProcess @@ -224,20 +226,17 @@ def unzip(archive, destdir): outfile.write(zfobj.read(name)) outfile.close() +@deprecated('Use subprocess.Popen instead') class Execute: """This is a deadlock safe version of popen2 (no stdin), that returns an object with errorlevel, out and err. """ def __init__(self, command): - outfile = tempfile.mktemp() - errfile = tempfile.mktemp() - self.status = os.system("( %s ) >%s 2>%s" % - (command, outfile, errfile)) >> 8 - self.out = open(outfile, "r").read() - self.err = open(errfile, "r").read() - os.remove(outfile) - os.remove(errfile) + cmd = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.out, self.err = cmd.communicate() + self.status = os.WEXITSTATUS(cmd.returncode) + def acquire_lock(lock_file, max_try=10, delay=10, max_delay=3600): """Acquire a lock represented by a file on the file system @@ -48,6 +48,8 @@ class Table(object): else: return list(self) == list(other) + __hash__ = object.__hash__ + def __ne__(self, other): return not self == other diff --git a/tasksqueue.py b/tasksqueue.py index e95a77e..71b57c1 100644 --- a/tasksqueue.py +++ b/tasksqueue.py @@ -94,5 +94,7 @@ class Task(object): def __eq__(self, other): return self.id == other.id + __hash__ = object.__hash__ + def merge(self, other): pass diff --git a/test/unittest_configuration.py b/test/unittest_configuration.py index 6798935..a8e3c0f 100644 --- a/test/unittest_configuration.py +++ b/test/unittest_configuration.py @@ -18,6 +18,7 @@ import tempfile import os from os.path import join, dirname, abspath +import re from cStringIO import StringIO from sys import version_info @@ -25,19 +26,20 @@ from sys import version_info from logilab.common.testlib import TestCase, unittest_main from logilab.common.optik_ext import OptionValueError from logilab.common.configuration import Configuration, \ - OptionsManagerMixIn, OptionsProviderMixIn, Method, read_old_config + OptionsManagerMixIn, OptionsProviderMixIn, Method, read_old_config, \ + merge_options DATA = join(dirname(abspath(__file__)), 'data') -options = [('dothis', {'type':'yn', 'action': 'store', 'default': True, 'metavar': '<y or n>'}), +OPTIONS = [('dothis', {'type':'yn', 'action': 'store', 'default': True, 'metavar': '<y or n>'}), ('value', {'type': 'string', 'metavar': '<string>', 'short': 'v'}), - ('multiple', {'type': 'csv', 'default': ('yop', 'yep'), + ('multiple', {'type': 'csv', 'default': ['yop', 'yep'], 'metavar': '<comma separated values>', 'help': 'you can also document the option'}), ('number', {'type': 'int', 'default':2, 'metavar':'<int>', 'help': 'boom'}), ('choice', {'type': 'choice', 'default':'yo', 'choices': ('yo', 'ye'), 'metavar':'<yo|ye>'}), - ('multiple-choice', {'type': 'multiple_choice', 'default':('yo', 'ye'), + ('multiple-choice', {'type': 'multiple_choice', 'default':['yo', 'ye'], 'choices': ('yo', 'ye', 'yu', 'yi', 'ya'), 'metavar':'<yo|ye>'}), ('named', {'type':'named', 'default':Method('get_named'), @@ -56,16 +58,16 @@ class MyConfiguration(Configuration): class ConfigurationTC(TestCase): def setUp(self): - self.cfg = MyConfiguration(name='test', options=options, usage='Just do it ! (tm)') + self.cfg = MyConfiguration(name='test', options=OPTIONS, usage='Just do it ! (tm)') def test_default(self): cfg = self.cfg self.assertEqual(cfg['dothis'], True) self.assertEqual(cfg['value'], None) - self.assertEqual(cfg['multiple'], ('yop', 'yep')) + self.assertEqual(cfg['multiple'], ['yop', 'yep']) self.assertEqual(cfg['number'], 2) self.assertEqual(cfg['choice'], 'yo') - self.assertEqual(cfg['multiple-choice'], ('yo', 'ye')) + self.assertEqual(cfg['multiple-choice'], ['yo', 'ye']) self.assertEqual(cfg['named'], {'key': 'val'}) def test_base(self): @@ -201,14 +203,17 @@ named=key:val diffgroup=pouet""") - def test_loopback(self): + def test_roundtrip(self): cfg = self.cfg f = tempfile.mktemp() stream = open(f, 'w') try: + self.cfg['dothis'] = False + self.cfg['multiple'] = ["toto", "tata"] + self.cfg['number'] = 3 cfg.generate_config(stream) stream.close() - new_cfg = MyConfiguration(name='testloop', options=options) + new_cfg = MyConfiguration(name='test', options=OPTIONS) new_cfg.load_file_configuration(f) self.assertEqual(cfg['dothis'], new_cfg['dothis']) self.assertEqual(cfg['multiple'], new_cfg['multiple']) @@ -233,18 +238,19 @@ diffgroup=pouet""") # it is not unlikely some optik/optparse versions do print -v<string> # so accept both help = help.replace(' -v <string>, ', ' -v<string>, ') + help = re.sub('[ ]*(\r?\n)', '\\1', help) USAGE = """Usage: Just do it ! (tm) Options: -h, --help show this help message and exit - --dothis=<y or n> + --dothis=<y or n> -v<string>, --value=<string> --multiple=<comma separated values> you can also document the option [current: yop,yep] --number=<int> boom [current: 2] - --choice=<yo|ye> + --choice=<yo|ye> --multiple-choice=<yo|ye> - --named=<key=val> + --named=<key=val> Agroup: --diffgroup=<key=val> @@ -329,6 +335,25 @@ class RegrTC(TestCase): self.linter.load_command_line_configuration([]) self.assertEqual(self.linter.config.profile, False) +class MergeTC(TestCase): + + def test_merge1(self): + merged = merge_options([('dothis', {'type':'yn', 'action': 'store', 'default': True, 'metavar': '<y or n>'}), + ('dothis', {'type':'yn', 'action': 'store', 'default': False, 'metavar': '<y or n>'}), + ]) + self.assertEqual(len(merged), 1) + self.assertEqual(merged[0][0], 'dothis') + self.assertEqual(merged[0][1]['default'], True) + + def test_merge2(self): + merged = merge_options([('dothis', {'type':'yn', 'action': 'store', 'default': True, 'metavar': '<y or n>'}), + ('value', {'type': 'string', 'metavar': '<string>', 'short': 'v'}), + ('dothis', {'type':'yn', 'action': 'store', 'default': False, 'metavar': '<y or n>'}), + ]) + self.assertEqual(len(merged), 2) + self.assertEqual(merged[0][0], 'value') + self.assertEqual(merged[1][0], 'dothis') + self.assertEqual(merged[1][1]['default'], True) if __name__ == '__main__': unittest_main() diff --git a/test/unittest_date.py b/test/unittest_date.py index 0aa1de8..ba1522c 100644 --- a/test/unittest_date.py +++ b/test/unittest_date.py @@ -138,6 +138,13 @@ class DateTC(TestCase): date = ticks2datetime(ticks) self.assertEqual(ustrftime(date, '%Y-%m-%d'), u'1899-12-31') + def test_month(self): + """enumerate months""" + r = list(date_range(self.datecls(2006, 5, 6), self.datecls(2006, 8, 27), + incmonth=True)) + expected = [self.datecls(2006, 5, 6), self.datecls(2006, 6, 1), self.datecls(2006, 7, 1), self.datecls(2006, 8, 1)] + self.assertListEqual(expected, r) + class MxDateTC(DateTC): datecls = mxDate diff --git a/test/unittest_deprecation.py b/test/unittest_deprecation.py index 7596317..ad268e8 100644 --- a/test/unittest_deprecation.py +++ b/test/unittest_deprecation.py @@ -78,5 +78,66 @@ class RawInputTC(TestCase): self.assertEqual(self.messages, ['object moving_target has been moved to module data.deprecation']) + def test_deprecated_manager(self): + deprecator = deprecation.DeprecationManager("module_name") + deprecator.compatibility('1.3') + # This warn should be printed. + deprecator.warn('1.1', "Major deprecation message.", 1) + deprecator.warn('1.1') + + @deprecator.deprecated('1.2', 'Major deprecation message.') + def any_func(): + pass + any_func() + + @deprecator.deprecated('1.2') + def other_func(): + pass + other_func() + + self.assertListEqual(self.messages, + ['[module_name 1.1] Major deprecation message.', + '[module_name 1.1] ', + '[module_name 1.2] Major deprecation message.', + '[module_name 1.2] The function "other_func" is deprecated']) + + def test_class_deprecated_manager(self): + deprecator = deprecation.DeprecationManager("module_name") + deprecator.compatibility('1.3') + class AnyClass: + __metaclass__ = deprecator.class_deprecated('1.2') + AnyClass() + self.assertEqual(self.messages, + ['[module_name 1.2] AnyClass is deprecated']) + + + def test_deprecated_manager_noprint(self): + deprecator = deprecation.DeprecationManager("module_name") + deprecator.compatibility('1.3') + # This warn should not be printed. + deprecator.warn('1.3', "Minor deprecation message.", 1) + + @deprecator.deprecated('1.3', 'Minor deprecation message.') + def any_func(): + pass + any_func() + + @deprecator.deprecated('1.20') + def other_func(): + pass + other_func() + + @deprecator.deprecated('1.4') + def other_func(): + pass + other_func() + + class AnyClass(object): + __metaclass__ = deprecator.class_deprecated((1,5)) + AnyClass() + + self.assertFalse(self.messages) + + if __name__ == '__main__': unittest_main() @@ -550,6 +550,9 @@ class TestCase(unittest.TestCase): func(*args, **kwargs) except (KeyboardInterrupt, SystemExit): raise + except unittest.SkipTest, e: + self._addSkip(result, str(e)) + return False except: result.addError(self, self.__exc_info()) return False @@ -1187,6 +1190,9 @@ succeeded test into", osp.join(os.getcwd(), FILE_RESTART) assertItemsEqual = unittest.TestCase.assertCountEqual else: assertCountEqual = unittest.TestCase.assertItemsEqual + if sys.version_info < (2,7): + def assertIsNotNone(self, value, *args, **kwargs): + self.assertNotEqual(None, value, *args, **kwargs) TestCase.assertItemsEqual = deprecated('assertItemsEqual is deprecated, use assertCountEqual')( TestCase.assertItemsEqual) |