diff options
author | Aurelien Campeas <aurelien.campeas@logilab.fr> | 2013-01-21 15:28:58 +0100 |
---|---|---|
committer | Aurelien Campeas <aurelien.campeas@logilab.fr> | 2013-01-21 15:28:58 +0100 |
commit | f73583632f36f0858b1932da3c40cc4c2d0b2b00 (patch) | |
tree | 219e872385dbb491be430962a2146738b9424513 | |
parent | 5fab8339edc52e3ea454e287cc1ac7eec8a20979 (diff) | |
parent | 20180f3add21b4dc93d728754a1a491793837709 (diff) | |
download | logilab-common-f73583632f36f0858b1932da3c40cc4c2d0b2b00.tar.gz |
[merge] default is stable
-rw-r--r-- | ChangeLog | 66 | ||||
-rw-r--r-- | __pkginfo__.py | 9 | ||||
-rw-r--r-- | configuration.py | 15 | ||||
-rw-r--r-- | debian/changelog | 12 | ||||
-rw-r--r-- | debian/copyright | 4 | ||||
-rw-r--r-- | debian/watch | 2 | ||||
-rw-r--r-- | deprecation.py | 2 | ||||
-rw-r--r-- | logging_ext.py | 15 | ||||
-rw-r--r-- | python-logilab-common.spec | 184 | ||||
-rw-r--r-- | registry.py | 448 | ||||
-rw-r--r-- | test/data/regobjects.py | 22 | ||||
-rw-r--r-- | test/data/regobjects2.py | 8 | ||||
-rw-r--r-- | test/unittest_deprecation.py | 22 | ||||
-rw-r--r-- | test/unittest_registry.py | 44 | ||||
-rw-r--r-- | umessage.py | 3 |
15 files changed, 671 insertions, 185 deletions
@@ -1,13 +1,57 @@ ChangeLog for logilab.common ============================ --- + +2013-01-21 -- 0.59.0 + + * registry: + + - introduce RegistrableObject base class, mandatory to make + 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) + + - deprecate usage of leading underscore to skip object registration, using + __abstract__ explicitly is better and notion of registered object 'name' + is now somewhat fuzzy + + - use register_all when no registration callback defined (closes #111011) + + * loggin_ext: on windows, use colorama to display colored logs, if available (closes #107436) + + * packaging: remove references to ftp at logilab + + * deprecations: really check them + + * packaging: steal spec file from fedora (closes #113099) + + * packaging force python2.6 on rhel5 (closes #113099) + + * packaging Update download and project urls (closes #113099) + + * configuration: enhance merge_options function (closes #113458) + + + +2012-11-14 -- 0.58.3 * date: fix ustrftime() impl. for python3 (closes #82161, patch by Arfrever Frehtes Taifersar Arahesis) and encoding detection for python2 (closes #109740) * other python3 code and test fixes (closes #104047) + * registry: Store.setdefault shouldn't raise RegistryNotFound (closes #111010) + + * table: stop encoding to iso-8859-1, use unicode (closes #105847) + + * setup: properly install additional files during build instead of install (closes #104045) + + + 2012-07-30 -- 0.58.2 * modutils: fixes (closes #100757 and #100935) @@ -294,7 +338,7 @@ ChangeLog for logilab.common - backup_command is now backup_commands (eg return a list of commands) - each command returned in backup_commands/restore_commands may now be list that may be used as argument to subprocess.call, or a string - which will the requires a subshell + which will the requires a subshell - new sql_rename_col method * deprecation: deprecated now takes an optional 'stacklevel' argument, default to 2 @@ -308,9 +352,9 @@ ChangeLog for logilab.common * db / adbh: added SQL Server support using Pyodbc * db: - - New optional extra_args argument to get_connection. - - Support Windows Auth for SQLServer by giving - extra_args='Trusted_Connection' to the sqlserver2005 driver + - New optional extra_args argument to get_connection. + - Support Windows Auth for SQLServer by giving + extra_args='Trusted_Connection' to the sqlserver2005 driver @@ -483,7 +527,7 @@ ChangeLog for logilab.common 2008-10-01 -- 0.35.2 * configuration: - fix #6011: lgc.configuration ignore customized option values - - fix #3278: man page generation broken + - fix #3278: man page generation broken * dropped context.py module which broke the debian package when some python <2.5 is installed (#5979) @@ -634,7 +678,7 @@ ChangeLog for logilab.common 2007-12-11 -- 0.25.1 * pytest: new --profile option, setup module / teardown module hook, - other fixes and enhancements + other fixes and enhancements * db: mysql support fixes @@ -683,7 +727,7 @@ ChangeLog for logilab.common meaning remaining args should not be checked * interface: new extend function to dynamically add an implemented interface - to a new style class + to a new style class @@ -755,7 +799,7 @@ ChangeLog for logilab.common has been added * deprecated fileutils.[files_by_ext,include_files_by_ext,exclude_files_by_ext] - functions in favor of new function shellutils.find + functions in favor of new function shellutils.find * mark the following modules for deprecation, they will be removed in a near version: @@ -984,8 +1028,8 @@ ChangeLog for logilab.common * shellutils: bug fix in mv() * compat: - - use set when available - - added sorted and reversed + - use set when available + - added sorted and reversed * table: new methods and some optimizations diff --git a/__pkginfo__.py b/__pkginfo__.py index 557cb59..3626c34 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -18,19 +18,19 @@ """logilab.common packaging information""" __docformat__ = "restructuredtext en" import sys +import os distname = 'logilab-common' modname = 'common' subpackage_of = 'logilab' subpackage_master = True -numversion = (0, 58, 2) +numversion = (0, 59, 0) version = '.'.join([str(num) for num in numversion]) license = 'LGPL' # 2.1 or later description = "collection of low-level Python packages and modules used by Logilab projects" web = "http://www.logilab.org/project/%s" % distname -ftp = "ftp://ftp.logilab.org/pub/%s" % modname mailinglist = "mailto://python-projects@lists.logilab.org" author = "Logilab" author_email = "contact@logilab.fr" @@ -40,8 +40,11 @@ from os.path import join scripts = [join('bin', 'pytest')] include_dirs = [join('test', 'data')] +install_requires = [] if sys.version_info < (2, 7): - install_requires = ['unittest2 >= 0.5.1'] + install_requires.append('unittest2 >= 0.5.1') +if os.name == 'nt': + install_requires.append('colorama') classifiers = ["Topic :: Utilities", "Programming Language :: Python", diff --git a/configuration.py b/configuration.py index 0eafa10..993f759 100644 --- a/configuration.py +++ b/configuration.py @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of logilab-common. @@ -1055,8 +1055,13 @@ def read_old_config(newconfig, changes, configfile): newconfig.set_option(optname, oldconfig[optname], optdict=optdef) -def merge_options(options): - """preprocess options to remove duplicate""" +def merge_options(options, optgroup=None): + """preprocess a list of options and remove duplicates, returning a new list + (tuple actually) of options. + + Options dictionaries are copied to avoid later side-effect. Also, if + `otpgroup` argument is specified, ensure all options are in the given group. + """ alloptions = {} options = list(options) for i in range(len(options)-1, -1, -1): @@ -1065,5 +1070,9 @@ def merge_options(options): options.pop(i) alloptions[optname].update(optdict) else: + optdict = optdict.copy() + options[i] = (optname, optdict) alloptions[optname] = optdict + if optgroup is not None: + alloptions[optname]['group'] = optgroup return tuple(options) diff --git a/debian/changelog b/debian/changelog index 8b11319..6a23e41 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +logilab-common (0.59.0-1) unstable; urgency=low + + * new upstream release + + -- Aurélien Campéas <aurelien.campeas@logilab.fr> Tue, 18 Dec 2012 12:35:00 +0100 + +logilab-common (0.58.3-1) unstable; urgency=low + + * new upstream release + + -- David Douard <david.douard@logilab.fr> Wed, 14 Nov 2012 16:18:53 +0100 + logilab-common (0.58.2-1) precise; urgency=low * new upstream release diff --git a/debian/copyright b/debian/copyright index b5ec0ae..bdac5cb 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,6 +1,6 @@ This package was debianized by Alexandre Fayolle <afayolle@debian.org> Sat, 13 Apr 2002 19:05:23 +0200. -It was downloaded from ftp://ftp.logilab.org/pub/common +It was downloaded from http://download.logilab.org/pub/common Upstream Author: @@ -8,7 +8,7 @@ Upstream Author: Copyright: - Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. + Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. http://www.logilab.fr/ -- mailto:contact@logilab.fr License: diff --git a/debian/watch b/debian/watch index 319a5c8..5f7776a 100644 --- a/debian/watch +++ b/debian/watch @@ -1,2 +1,2 @@ version=3 -opts=pasv ftp://ftp.logilab.org/pub/common/logilab-common-(.*)\.tar\.gz +http://download.logilab.org/pub/common/logilab-common-(.*)\.tar\.gz diff --git a/deprecation.py b/deprecation.py index c14bd2a..5e2f813 100644 --- a/deprecation.py +++ b/deprecation.py @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of logilab-common. diff --git a/logging_ext.py b/logging_ext.py index 1b7a1e6..e4d2490 100644 --- a/logging_ext.py +++ b/logging_ext.py @@ -132,9 +132,20 @@ def get_threshold(debug=False, logthreshold=None): logthreshold)) return logthreshold -def get_formatter(logformat=LOG_FORMAT, logdateformat=LOG_DATE_FORMAT): +def _colorable_terminal(): isatty = hasattr(sys.__stdout__, 'isatty') and sys.__stdout__.isatty() - if isatty and sys.platform != 'win32': + if not isatty: + return False + if os.name == 'nt': + try: + from colorama import init as init_win32_colors + except ImportError: + return False + init_win32_colors() + return True + +def get_formatter(logformat=LOG_FORMAT, logdateformat=LOG_DATE_FORMAT): + if _colorable_terminal(): fmt = ColorFormatter(logformat, logdateformat) def col_fact(record): if 'XXX' in record.message: diff --git a/python-logilab-common.spec b/python-logilab-common.spec new file mode 100644 index 0000000..0ee1282 --- /dev/null +++ b/python-logilab-common.spec @@ -0,0 +1,184 @@ +# for el5, force use of python2.6 +%if 0%{?el5} +%define python python26 +%define __python /usr/bin/python2.6 +%{!?python_scriptarch: %define python_scriptarch %(%{__python} -c "from distutils.sysconfig import get_python_lib; from os.path import join; print join(get_python_lib(1, 1), 'scripts')")} +%else +%define python python +%define __python /usr/bin/python +%endif +%{!?_python_sitelib: %define _python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} + +Name: %{python}-logilab-common +Version: 0.58.2 +Release: logilab.1%{?dist} +Summary: Common libraries for Logilab projects + +Group: Development/Libraries +License: GPLv2+ +URL: http://www.logilab.org/projects/logilab-common +Source0: http://download.logilab.org/pub/common/logilab-common-%{version}.tar.gz +BuildArch: noarch +BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) + +BuildRequires: python-devel python-setuptools python-unittest2 +Requires: mx + + +%description +This package contains several modules providing low level functionality +shared among some python projects developed by logilab. + + +%prep +%setup -q -n logilab-common-%{version} + + +%build +%{__python} setup.py build +%if 0%{?el5} +# change the python version in shebangs +find . -name '*.py' -type f -print0 | xargs -0 sed -i '1,3s;^#!.*python.*$;#! /usr/bin/python2.6;' +%endif + + +%install +rm -rf $RPM_BUILD_ROOT +NO_SETUPTOOLS=1 %{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT %{?python_scriptarch: --install-scripts=%{python_scriptarch}} +rm -rf $RPM_BUILD_ROOT%{_python_sitelib}/logilab/common/test + +%check +%{__python} setup.py test + +%clean +rm -rf $RPM_BUILD_ROOT + + +%files +%defattr(-,root,root,-) +%doc README ChangeLog COPYING +%{_python_sitelib}/logilab* +%{_bindir}/* + + +%changelog +* Fri Nov 16 2012 Julien Cristau <julien.cristau@logilab.fr> 0.58.2-logilab.1 +- Force python26 on el5 + +* Fri Aug 03 2012 Brian C. Lane <bcl@redhat.com> 0.58.2-1 +- Upstream 0.58.2 + +* Sat Jul 21 2012 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.57.1-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild + +* Sat Jan 14 2012 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.57.1-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_17_Mass_Rebuild + +* Fri Nov 18 2011 Brian C. Lane <bcl@redhat.com> - 0.57.1-1 +- Upstream 0.57.1 + +* Fri Jul 29 2011 Brian C. Lane <bcl@redhat.com> - 0.56.0-1 +- Upstream 0.56.0 + +* Mon Mar 28 2011 Brian C. Lane <bcl@redhat.com> - 0.55.1-1 +- Upstream 0.55.1 +- Add unit tests to spec + +* Tue Feb 08 2011 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.53.0-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild + +* Mon Nov 29 2010 Brian C. Lane <bcl@redhat.com> - 0.53.0-1 +- Upstream 0.53.0 + +* Thu Jul 22 2010 David Malcolm <dmalcolm@redhat.com> - 0.50.3-2 +- Rebuilt for https://fedoraproject.org/wiki/Features/Python_2.7/MassRebuild + +* Thu Jul 08 2010 Brian C. Lane <bcl@brdhat.com> - 0.50.3-1 +- Upstream 0.50.3 + +* Fri Mar 26 2010 Brian C. Lane <bcl@redhat.com> - 0.49.0-2 +- Add python-setuptools to BuildRequires + +* Thu Mar 25 2010 Brian C. Lane <bcl@redhat.com> - 0.49.0-1 +- Upstream 0.49.0 + +* Sun Aug 30 2009 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.45.0-1 +- Upstream 0.45.0 (small enhancements and bugfixes) + +* Sun Jul 26 2009 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.41.0-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_12_Mass_Rebuild + +* Wed Jun 17 2009 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.41.0-2 +- Upstream 0.41.0 +- Bugfixes and a few minor new features + +* Thu Feb 26 2009 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.38.0-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_11_Mass_Rebuild + +* Wed Jan 28 2009 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.38.0-1 +- Upstream 0.38.0 + +* Tue Dec 30 2008 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.37.0-1 +- Upstream 0.37.0 + +* Sat Nov 29 2008 Ignacio Vazquez-Abrams <ivazqueznet+rpm@gmail.com> - 0.32.0-2 +- Rebuild for Python 2.6 + +* Mon Jun 30 2008 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.32.0-1 +- Upstream 0.32.0 + +* Sun Feb 17 2008 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.28.0-1 +- Upstream 0.28.0 + +* Thu Jan 17 2008 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.26.1-1 +- Upstream 0.26.1 +- Package egg-info and other files. + +* Mon Dec 24 2007 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.25.2-1 +- Upstream 0.25.2 + +* Sun Nov 18 2007 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.24.0-1 +- Upstream 0.24.0 +- Adjust license to the new standard + +* Sun Apr 01 2007 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.21.2-1 +- Upstream 0.21.2 + +* Sun Dec 17 2006 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.21.0-1 +- Upstream 0.21.0 +- Include COPYING with docs + +* Tue Sep 26 2006 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.19.2-1 +- Upstream 0.19.2 +- Ghostbusting +- Require mx + +* Mon May 01 2006 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.15.0-1 +- Version 0.15.0 + +* Sun Mar 12 2006 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.14.1-2 +- Also handle __init__.pyc and __init__.pyo + +* Sun Mar 12 2006 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.14.1-1 +- Version 0.14.1 + +* Thu Jan 12 2006 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.13.0-1 +- Version 0.13.0 +- astng no longer part of the package + +* Thu Nov 17 2005 Konstantin Ryabitsev <icon@fedoraproject.org> - 0.12.0-1 +- Version 0.12.0 + +* Mon Jun 13 2005 Konstantin Ryabitsev <icon@linux.duke.edu> - 0.10.0-1 +- Version 0.10.0. +- Disttagging. + +* Thu May 05 2005 Konstantin Ryabitsev <icon@linux.duke.edu> - 0.9.3-3 +- Fix paths. + +* Tue Apr 26 2005 Konstantin Ryabitsev <icon@linux.duke.edu> - 0.9.3-2 +- Ghost .pyo files. +- Get rid of test, which doesn't do anything. + +* Fri Apr 22 2005 Konstantin Ryabitsev <icon@linux.duke.edu> - 0.9.3-1 +- Initial packaging. diff --git a/registry.py b/registry.py index cebed8e..fec35ad 100644 --- a/registry.py +++ b/registry.py @@ -78,11 +78,15 @@ __docformat__ = "restructuredtext en" import sys import types import weakref +import traceback as tb from os import listdir, stat from os.path import join, isdir, exists from logging import getLogger +from warnings import warn +from logilab.common.modutils import modpath_from_file from logilab.common.logging_ext import set_log_methods +from logilab.common.decorators import classproperty class RegistryException(Exception): @@ -112,11 +116,28 @@ class NoSelectableObject(RegistryException): % (self.args, self.kwargs.keys(), self.objects)) +def _modname_from_path(path, extrapath=None): + modpath = modpath_from_file(path, extrapath) + # omit '__init__' from package's name to avoid loading that module + # once for each name when it is imported by some other object + # module. This supposes import in modules are done as:: + # + # from package import something + # + # not:: + # + # from package.__init__ import something + # + # which seems quite correct. + if modpath[-1] == '__init__': + modpath.pop() + return '.'.join(modpath) + + def _toload_info(path, extrapath, _toload=None): """Return a dictionary of <modname>: <modpath> and an ordered list of (file, module name) to load """ - from logilab.common.modutils import modpath_from_file if _toload is None: assert isinstance(path, list) _toload = {}, [] @@ -125,35 +146,62 @@ def _toload_info(path, extrapath, _toload=None): subfiles = [join(fileordir, fname) for fname in listdir(fileordir)] _toload_info(subfiles, extrapath, _toload) elif fileordir[-3:] == '.py': - modpath = modpath_from_file(fileordir, extrapath) - # omit '__init__' from package's name to avoid loading that module - # once for each name when it is imported by some other object - # module. This supposes import in modules are done as:: - # - # from package import something - # - # not:: - # - # from package.__init__ import something - # - # which seems quite correct. - if modpath[-1] == '__init__': - modpath.pop() - modname = '.'.join(modpath) + modname = _modname_from_path(fileordir, extrapath) _toload[0][modname] = fileordir _toload[1].append((fileordir, modname)) return _toload -def classid(cls): - """returns a unique identifier for an object class""" - return '%s.%s' % (cls.__module__, cls.__name__) +class RegistrableObject(object): + """This is the base class for registrable objects which are selected + according to a context. -def class_registries(cls, registryname): - """return a tuple of registry names (see __registries__)""" - if registryname: - return (registryname,) - return cls.__registries__ + :attr:`__registry__` + name of the registry for this object (string like 'views', + 'templates'...). You may want to define `__registries__` directly if your + object should be registered in several registries. + + :attr:`__regid__` + object's identifier in the registry (string like 'main', + 'primary', 'folder_box') + + :attr:`__select__` + class'selector + + Moreover, the `__abstract__` attribute may be set to True to indicate that a + class is abstract and should not be registered. + + You don't have to inherit from this class to put it in a registry (having + `__regid__` and `__select__` is enough), though this is needed for classes + that should be automatically registered. + """ + + __registry__ = None + __regid__ = None + __select__ = None + __abstract__ = True # see doc snipppets below (in Registry class) + + @classproperty + def __registries__(cls): + if cls.__registry__ is None: + return () + return (cls.__registry__,) + + +class RegistrableInstance(RegistrableObject): + """Inherit this class if you want instances of the classes to be + automatically registered. + """ + + def __new__(cls, *args, **kwargs): + """Add a __module__ attribute telling the module where the instance was + created, for automatic registration. + """ + obj = super(RegistrableInstance, cls).__new__(cls) + # XXX subclass must no override __new__ + filepath = tb.extract_stack(limit=2)[0][0] + obj.__module__ = _modname_from_path(filepath) + return obj class Registry(dict): @@ -202,6 +250,16 @@ class Registry(dict): except KeyError: raise ObjectNotFound(name), None, sys.exc_info()[-1] + @classmethod + def objid(cls, obj): + """returns a unique identifier for an object stored in the registry""" + return '%s.%s' % (obj.__module__, cls.objname(obj)) + + @classmethod + def objname(cls, obj): + """returns a readable name for an object stored in the registry""" + return getattr(obj, '__name__', id(obj)) + def initialization_completed(self): """call method __registered__() on registered objects when the callback is defined""" @@ -215,16 +273,16 @@ class Registry(dict): def register(self, obj, oid=None, clear=False): """base method to add an object in the registry""" - assert not '__abstract__' in obj.__dict__ - assert obj.__select__ + assert not '__abstract__' in obj.__dict__, obj + assert obj.__select__, obj oid = oid or obj.__regid__ - assert oid + assert oid, ('no explicit name supplied to register object %s, ' + 'which has no __regid__ set' % obj) if clear: objects = self[oid] = [] else: objects = self.setdefault(oid, []) - assert not obj in objects, \ - 'object %s is already registered' % obj + assert not obj in objects, 'object %s is already registered' % obj objects.append(obj) def register_and_replace(self, obj, replaced): @@ -233,12 +291,12 @@ class Registry(dict): # remove register_and_replace in favor of unregister + register # or simplify by calling unregister then register here if not isinstance(replaced, basestring): - replaced = classid(replaced) + replaced = self.objid(replaced) # prevent from misspelling assert obj is not replaced, 'replacing an object by itself: %s' % obj registered_objs = self.get(obj.__regid__, ()) for index, registered in enumerate(registered_objs): - if classid(registered) == replaced: + if self.objid(registered) == replaced: del registered_objs[index] break else: @@ -248,17 +306,17 @@ class Registry(dict): def unregister(self, obj): """remove object <obj> from this registry""" - clsid = classid(obj) + objid = self.objid(obj) oid = obj.__regid__ for registered in self.get(oid, ()): - # use classid() to compare classes because vreg will probably - # have its own version of the class, loaded through execfile - if classid(registered) == clsid: + # use self.objid() to compare objects because vreg will probably + # have its own version of the object, loaded through execfile + if self.objid(registered) == objid: self[oid].remove(registered) break else: self.warning('can\'t remove %s, no id %s in the registry', - clsid, oid) + objid, oid) def all_objects(self): """return a list containing all objects in this registry. @@ -339,50 +397,63 @@ class Registry(dict): raise Exception(msg % (winners, args, kwargs.keys())) self.error(msg, winners, args, kwargs.keys()) # return the result of calling the object - return winners[0](*args, **kwargs) + return self.selected(winners[0], args, kwargs) + + def selected(self, winner, args, kwargs): + """override here if for instance you don't want "instanciation" + """ + return winner(*args, **kwargs) # these are overridden by set_log_methods below # only defining here to prevent pylint from complaining info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None +def obj_registries(cls, registryname=None): + """return a tuple of registry names (see __registries__)""" + if registryname: + return (registryname,) + return cls.__registries__ + + class RegistryStore(dict): - """This class is responsible for loading implementations and storing them - in their registry which are created on the fly as needed. + """This class is responsible for loading objects and storing them + in their registry which is created on the fly as needed. - It handles dynamic registration of objects and provides a convenient api to - access them. To be recognized as an object that should be stored into one of - the store's registry (:class:`Registry`), an object (usually a class) has - the following attributes, used control how they interact with the registry: + It handles dynamic registration of objects and provides a + convenient api to access them. To be recognized as an object that + should be stored into one of the store's registry + (:class:`Registry`), an object must provide the following + attributes, used control how they interact with the registry: - :attr:`__registry__` or `__registries__` - name of the registry for this object (string like 'views', 'templates'...) - or list of registry names if you want your object to be added to multiple - registries + :attr:`__registries__` + list of registry names (string like 'views', 'templates'...) into which + the object should be registered :attr:`__regid__` - implementation's identifier in the registry (string like 'main', + object identifier in the registry (string like 'main', 'primary', 'folder_box') :attr:`__select__` - the implementation's selector + the object predicate selectors - Moreover, the :attr:`__abstract__` attribute may be set to `True` to - indicate that a class is abstract and should not be registered (inherited - attributes not considered). + Moreover, the :attr:`__abstract__` attribute may be set to `True` + to indicate that an object is abstract and should not be registered + (such inherited attributes not considered). .. Note:: When using the store to load objects dynamically, you *always* have to use **super()** to get the methods and attributes of the - superclasses, and not use the class identifier. Else, you'll get into - trouble when reloading comes into the place. + superclasses, and not use the class identifier. If not, you'll get into + trouble at reload time. For example, instead of writing:: class Thing(Parent): __regid__ = 'athing' __select__ = yes() + def f(self, arg1): Parent.f(self, arg1) @@ -391,22 +462,25 @@ class RegistryStore(dict): class Thing(Parent): __regid__ = 'athing' __select__ = yes() + def f(self, arg1): - super(Parent, self).f(arg1) + super(Thing, self).f(arg1) - Controlling objects registration - -------------------------------- + Controlling object registration + ------------------------------- - Dynamic loading is triggered by calling the :meth:`register_objects` method, - given a list of directory to inspect for python modules. + Dynamic loading is triggered by calling the + :meth:`register_objects` method, given a list of directories to + inspect for python modules. .. automethod: register_objects For each module, by default, all compatible objects are registered - automatically, though if some objects have to replace other objects, or have - to be included only if some condition is met, you'll have to define a - `registration_callback(vreg)` function in your module and explicitly - register **all objects** in this module, using the api defined below. + automatically. However if some objects come as replacement of + other objects, or have to be included only if some condition is + met, you'll have to define a `registration_callback(vreg)` + function in the module and explicitly register **all objects** in + this module, using the api defined below. .. automethod:: RegistryStore.register_all @@ -417,51 +491,48 @@ class RegistryStore(dict): .. Note:: Once the function `registration_callback(vreg)` is implemented in a module, all the objects from this module have to be explicitly - registered as it disables the automatic objects registration. + registered as it disables the automatic object registration. Examples: .. sourcecode:: python - # cubicweb/web/views/basecomponents.py def registration_callback(store): - # register everything in the module except SeeAlsoComponent - store.register_all(globals().values(), __name__, (SeeAlsoVComponent,)) - # conditionally register SeeAlsoVComponent - if 'see_also' in store.schema: - store.register(SeeAlsoVComponent) + # register everything in the module except BabarClass + store.register_all(globals().values(), __name__, (BabarClass,)) + + # conditionally register BabarClass + if 'babar_relation' in store.schema: + store.register(BabarClass) In this example, we register all application object classes defined in the module - except `SeeAlsoVComponent`. This class is then registered only if the 'see_also' - relation type is defined in the instance'schema. + except `BabarClass`. This class is then registered only if the 'babar_relation' + relation type is defined in the instance schema. .. sourcecode:: python - # goa/appobjects/sessions.py def registration_callback(store): - store.register(SessionsCleaner) - # replace AuthenticationManager by GAEAuthenticationManager - store.register_and_replace(GAEAuthenticationManager, AuthenticationManager) - # replace PersistentSessionManager by GAEPersistentSessionManager - store.register_and_replace(GAEPersistentSessionManager, PersistentSessionManager) + store.register(Elephant) + # replace Babar by Celeste + store.register_and_replace(Celeste, Babar) In this example, we explicitly register classes one by one: - * the `SessionCleaner` class - * the `GAEAuthenticationManager` to replace the `AuthenticationManager` - * the `GAEPersistentSessionManager` to replace the `PersistentSessionManager` + * the `Elephant` class + * the `Celeste` to replace `Babar` If at some point we register a new appobject class in this module, it won't be registered at all without modification to the `registration_callback` - implementation. The previous example will register it though, thanks to the call + implementation. The first example will register it though, thanks to the call to the `register_all` method. - Controlling registry instantation - --------------------------------- + Controlling registry instantiation + ---------------------------------- + The `REGISTRY_FACTORY` class dictionary allows to specify which class should - be instantiated for a given registry name. The class associated to `None` in - it will be the class used when there is no specific class for a name. + be instantiated for a given registry name. The class associated to `None` + key will be the class used when there is no specific class for a name. """ def __init__(self, debugmode=False): @@ -500,12 +571,15 @@ class RegistryStore(dict): def setdefault(self, regid): try: return self[regid] - except KeyError: + except RegistryNotFound: self[regid] = self.registry_class(regid)(self.debugmode) return self[regid] def register_all(self, objects, modname, butclasses=()): - """register all `objects` given. Objects which are not from the module + """register registrable objects into `objects`. + + Registrable objects are properly configured subclasses of + :class:`RegistrableObject`. Objects which are not defined in the module `modname` or which are in `butclasses` won't be registered. Typical usage is: @@ -516,57 +590,56 @@ class RegistryStore(dict): So you get partially automatic registration, keeping manual registration for some object (to use - :meth:`~logilab.common.registry.RegistryStore.register_and_replace` - for instance) + :meth:`~logilab.common.registry.RegistryStore.register_and_replace` for + instance). """ assert isinstance(modname, basestring), \ 'modname expected to be a module name (ie string), got %r' % modname for obj in objects: - try: - if obj.__module__ != modname or obj in butclasses: - continue - oid = obj.__regid__ - except AttributeError: - continue - if oid and not obj.__dict__.get('__abstract__'): - self.register(obj, oid=oid) + if self.is_registrable(obj) and obj.__module__ == modname and not obj in butclasses: + if isinstance(obj, type): + self._load_ancestors_then_object(modname, obj, butclasses) + else: + self.register(obj) def register(self, obj, registryname=None, oid=None, clear=False): """register `obj` implementation into `registryname` or - `obj.__registry__` if not specified, with identifier `oid` or + `obj.__registries__` if not specified, with identifier `oid` or `obj.__regid__` if not specified. If `clear` is true, all objects with the same identifier will be previously unregistered. """ - assert not obj.__dict__.get('__abstract__') - try: - vname = obj.__name__ - except AttributeError: - # XXX may occurs? - vname = obj.__class__.__name__ - for registryname in class_registries(obj, registryname): + assert not obj.__dict__.get('__abstract__'), obj + for registryname in obj_registries(obj, registryname): registry = self.setdefault(registryname) registry.register(obj, oid=oid, clear=clear) - self.debug('register %s in %s[\'%s\']', - vname, registryname, oid or obj.__regid__) - self._loadedmods.setdefault(obj.__module__, {})[classid(obj)] = obj + self.debug("register %s in %s['%s']", + registry.objname(obj), registryname, oid or obj.__regid__) + self._loadedmods.setdefault(obj.__module__, {})[registry.objid(obj)] = obj def unregister(self, obj, registryname=None): - """unregister `obj` implementation object from the registry - `registryname` or `obj.__registry__` if not specified. + """unregister `obj` object from the registry `registryname` or + `obj.__registries__` if not specified. """ - for registryname in class_registries(obj, registryname): - self[registryname].unregister(obj) + for registryname in obj_registries(obj, registryname): + registry = self[registryname] + registry.unregister(obj) + self.debug("unregister %s from %s['%s']", + registry.objname(obj), registryname, obj.__regid__) def register_and_replace(self, obj, replaced, registryname=None): - """register `obj` implementation object into `registryname` or - `obj.__registry__` if not specified. If found, the `replaced` object - will be unregistered first (else a warning will be issued as it's + """register `obj` object into `registryname` or + `obj.__registries__` if not specified. If found, the `replaced` object + will be unregistered first (else a warning will be issued as it is generally unexpected). """ - for registryname in class_registries(obj, registryname): - self[registryname].register_and_replace(obj, replaced) + for registryname in obj_registries(obj, registryname): + registry = self[registryname] + registry.register_and_replace(obj, replaced) + self.debug("register %s in %s['%s'] instead of %s", + registry.objname(obj), registryname, obj.__regid__, + registry.objname(replaced)) # initialization methods ################################################### @@ -598,6 +671,7 @@ class RegistryStore(dict): reg.initialization_completed() def _mdate(self, filepath): + """ return the modification date of a file path """ try: return stat(filepath)[-2] except OSError: @@ -629,7 +703,7 @@ class RegistryStore(dict): return False def load_file(self, filepath, modname): - """load app objects from a python file""" + """ load registrable objects (if any) from a python file """ from logilab.common.modutils import load_module_from_name if modname in self._loadedmods: return @@ -649,59 +723,107 @@ class RegistryStore(dict): self.load_module(module) def load_module(self, module): - """load objects from a module using registration_callback() when it exists + """Automatically handle module objects registration. + + Instances are registered as soon as they are hashable and have the + following attributes: + + * __regid__ (a string) + * __select__ (a callable) + * __registries__ (a tuple/list of string) + + For classes this is a bit more complicated : + + - first ensure parent classes are already registered + + - class with __abstract__ == True in their local dictionary are skipped + + - object class needs to have registries and identifier properly set to a + non empty string to be registered. """ self.info('loading %s from %s', module.__name__, module.__file__) if hasattr(module, 'registration_callback'): module.registration_callback(self) else: - for objname, obj in vars(module).items(): - if objname.startswith('_'): - continue - self._load_ancestors_then_object(module.__name__, obj) - - def _load_ancestors_then_object(self, modname, objectcls): - """handle automatic object class registration: - - - first ensure parent classes are already registered - - - class with __abstract__ == True in their local dictionary or - with a name starting with an underscore are not registered + self.register_all(vars(module).itervalues(), module.__name__) - - object class needs to have __registry__ and __regid__ attributes - set to a non empty string to be registered. + def _load_ancestors_then_object(self, modname, objectcls, butclasses=()): + """handle class registration according to rules defined in + :meth:`load_module` """ + # backward compat, we used to allow whatever else than classes + if not isinstance(objectcls, type): + if self.is_registrable(objectcls) and objectcls.__module__ == modname: + self.register(objectcls) + return # imported classes - objmodname = getattr(objectcls, '__module__', None) + objmodname = objectcls.__module__ if objmodname != modname: + # The module of the object is not the same as the currently + # worked on module, or this is actually an instance, which + # has no module at all if objmodname in self._toloadmods: + # if this is still scheduled for loading, let's proceed immediately, + # but using the object module self.load_file(self._toloadmods[objmodname], objmodname) return - # skip non registerable object - try: - if not (getattr(objectcls, '__regid__', None) - and getattr(objectcls, '__select__', None)): - return - except TypeError: - return - clsid = classid(objectcls) + # ensure object hasn't been already processed + clsid = '%s.%s' % (modname, objectcls.__name__) if clsid in self._loadedmods[modname]: return self._loadedmods[modname][clsid] = objectcls + # ensure ancestors are registered for parent in objectcls.__bases__: - self._load_ancestors_then_object(modname, parent) - if (objectcls.__dict__.get('__abstract__') - or objectcls.__name__[0] == '_' - or not objectcls.__registries__ - or not objectcls.__regid__): + self._load_ancestors_then_object(modname, parent, butclasses) + # ensure object is registrable + if objectcls in butclasses or not self.is_registrable(objectcls): return - try: - self.register(objectcls) - except Exception, ex: - if self.debugmode: - raise - self.exception('object %s registration failed: %s', - objectcls, ex) + # backward compat + reg = self.setdefault(obj_registries(objectcls)[0]) + if reg.objname(objectcls)[0] == '_': + warn("[lgc 0.59] object whose name start with '_' won't be " + "skipped anymore at some point, use __abstract__ = True " + "instead (%s)" % objectcls, DeprecationWarning) + return + # register, finally + self.register(objectcls) + + @classmethod + def is_registrable(cls, obj): + """ensure `obj` should be registered + + as arbitrary stuff may be registered, do a lot of check and warn about + weird cases (think to dumb proxy objects) + """ + if isinstance(obj, type): + if not issubclass(obj, RegistrableObject): + # ducktyping backward compat + if not (getattr(obj, '__registries__', None) + and getattr(obj, '__regid__', None) + and getattr(obj, '__select__', None)): + return False + elif issubclass(obj, RegistrableInstance): + return False + elif not isinstance(obj, RegistrableInstance): + return False + if not obj.__regid__: + return False # no regid + registries = obj.__registries__ + if not registries: + return False # no registries + selector = obj.__select__ + if not selector: + return False # no selector + if obj.__dict__.get('__abstract__', False): + return False + # then detect potential problems that should be warned + if not isinstance(registries, (tuple, list)): + cls.warning('%s has __registries__ which is not a list or tuple', obj) + return False + if not callable(selector): + cls.warning('%s has not callable __select__', obj) + return False + return True # these are overridden by set_log_methods below # only defining here to prevent pylint from complaining @@ -750,7 +872,7 @@ class traced_selection(object): # pylint: disable=C0103 This will yield lines like this in the logs:: - selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'> + selector one_line_rset returned 0 for <class 'elephant.Babar'> You can also give to :class:`traced_selection` the identifiers of objects on which you want to debug selection ('oid1' and 'oid2' in the example above). @@ -973,3 +1095,17 @@ class yes(Predicate): # pylint: disable=C0103 def __call__(self, *args, **kwargs): return self.score + + +# deprecated stuff ############################################################# + +from logilab.common.deprecation import deprecated + +@deprecated('[lgc 0.59] use Registry.objid class method instead') +def classid(cls): + return '%s.%s' % (cls.__module__, cls.__name__) + +@deprecated('[lgc 0.59] use obj_registries function instead') +def class_registries(cls, registryname): + return obj_registries(cls, registryname) + diff --git a/test/data/regobjects.py b/test/data/regobjects.py new file mode 100644 index 0000000..6cea558 --- /dev/null +++ b/test/data/regobjects.py @@ -0,0 +1,22 @@ +"""unittest_registry data file""" +from logilab.common.registry import yes, RegistrableObject, RegistrableInstance + +class Proxy(object): + """annoying object should that not be registered, nor cause error""" + def __getattr__(self, attr): + return 1 + +trap = Proxy() + +class AppObjectClass(RegistrableObject): + __registry__ = 'zereg' + __regid__ = 'appobject1' + __select__ = yes() + +class AppObjectInstance(RegistrableInstance): + __registry__ = 'zereg' + __select__ = yes() + def __init__(self, regid): + self.__regid__ = regid + +appobject2 = AppObjectInstance('appobject2') diff --git a/test/data/regobjects2.py b/test/data/regobjects2.py new file mode 100644 index 0000000..5c28b51 --- /dev/null +++ b/test/data/regobjects2.py @@ -0,0 +1,8 @@ +from logilab.common.registry import RegistrableObject, RegistrableInstance, yes + +class MyRegistrableInstance(RegistrableInstance): + __regid__ = 'appobject3' + __select__ = yes() + __registry__ = 'zereg' + +instance = MyRegistrableInstance() diff --git a/test/unittest_deprecation.py b/test/unittest_deprecation.py index d697250..7596317 100644 --- a/test/unittest_deprecation.py +++ b/test/unittest_deprecation.py @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of logilab-common. @@ -28,10 +28,16 @@ class RawInputTC(TestCase): # XXX with 2.6 we could test warnings # http://docs.python.org/library/warnings.html#testing-warnings # instead we just make sure it does not crash + + def mock_warn(self, *args, **kwargs): + self.messages.append(args[0]) + def setUp(self): - warnings.simplefilter("ignore") + self.messages = [] + deprecation.warn = self.mock_warn + def tearDown(self): - warnings.simplefilter("default") + deprecation.warn = warnings.warn def mk_func(self): def any_func(): @@ -41,28 +47,36 @@ class RawInputTC(TestCase): def test_class_deprecated(self): class AnyClass: __metaclass__ = deprecation.class_deprecated + AnyClass() + self.assertEqual(self.messages, + ['AnyClass is deprecated']) def test_deprecated_func(self): any_func = deprecation.deprecated()(self.mk_func()) any_func() any_func = deprecation.deprecated('message')(self.mk_func()) any_func() + self.assertEqual(self.messages, + ['The function "any_func" is deprecated', 'message']) def test_deprecated_decorator(self): @deprecation.deprecated() def any_func(): pass any_func() - @deprecation.deprecated('message') def any_func(): pass any_func() + self.assertEqual(self.messages, + ['The function "any_func" is deprecated', 'message']) def test_moved(self): module = 'data.deprecation' any_func = deprecation.moved(module, 'moving_target') any_func() + self.assertEqual(self.messages, + ['object moving_target has been moved to module data.deprecation']) if __name__ == '__main__': unittest_main() diff --git a/test/unittest_registry.py b/test/unittest_registry.py index b84f672..a15fe98 100644 --- a/test/unittest_registry.py +++ b/test/unittest_registry.py @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of Logilab-Common. @@ -19,10 +19,17 @@ from __future__ import with_statement import gc +import logging +import os.path as osp +import sys from operator import eq, lt, le, gt +from contextlib import contextmanager + +logging.basicConfig(level=logging.ERROR) + from logilab.common.testlib import TestCase, unittest_main -from logilab.common.registry import Predicate, AndPredicate, OrPredicate, wrap_predicates +from logilab.common.registry import * class _1_(Predicate): @@ -159,5 +166,38 @@ class SelectorsTC(TestCase): self.assertEqual(s3(None), 0) self.assertEqual(self.count, 8) +@contextmanager +def prepended_syspath(path): + sys.path.insert(0, path) + yield + sys.path = sys.path[1:] + +class RegistryStoreTC(TestCase): + + def test_autoload(self): + store = RegistryStore() + store.setdefault('zereg') + with prepended_syspath(self.datadir): + store.register_objects([self.datapath('regobjects.py'), + self.datapath('regobjects2.py')]) + self.assertEqual(['zereg'], store.keys()) + self.assertEqual(set(('appobject1', 'appobject2', 'appobject3')), + set(store['zereg'])) + + +class RegistrableInstanceTC(TestCase): + + def test_instance_modulename(self): + # no inheritance + obj = RegistrableInstance() + self.assertEqual(obj.__module__, 'unittest_registry') + # with inheritance from another python file + with prepended_syspath(self.datadir): + from regobjects2 import instance, MyRegistrableInstance + instance2 = MyRegistrableInstance() + self.assertEqual(instance.__module__, 'regobjects2') + self.assertEqual(instance2.__module__, 'unittest_registry') + + if __name__ == '__main__': unittest_main() diff --git a/umessage.py b/umessage.py index 3e6fb37..c597c17 100644 --- a/umessage.py +++ b/umessage.py @@ -75,6 +75,9 @@ class UMessage: return decode_QP(value) return value + def __getitem__(self, header): + return self.get(header) + def get_all(self, header, default=()): return [decode_QP(val) for val in self.message.get_all(header, default) if val is not None] |