From 909df133635226ba3e622ea6b0130a541416664b Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 13 Nov 2012 07:32:23 +0100 Subject: [registry] enhance assertion error message --- registry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/registry.py b/registry.py index cebed8e..64f59e4 100644 --- a/registry.py +++ b/registry.py @@ -218,7 +218,8 @@ class Registry(dict): assert not '__abstract__' in obj.__dict__ assert obj.__select__ 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: -- cgit v1.2.1 From 9e405b2e2fddbf380dff6c3551297567b42bcab2 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 13 Nov 2012 07:33:44 +0100 Subject: [registry] setdefault catch wrong exception. Closes #111010 --- ChangeLog | 3 +++ registry.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 7ce11d3..4ce2512 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,9 @@ ChangeLog for logilab.common * other python3 code and test fixes (closes #104047) + * registry: setdefault shouldn't raise RegistryNotFound (closes #111010) + + 2012-07-30 -- 0.58.2 * modutils: fixes (closes #100757 and #100935) diff --git a/registry.py b/registry.py index 64f59e4..86065c5 100644 --- a/registry.py +++ b/registry.py @@ -501,7 +501,7 @@ 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] -- cgit v1.2.1 From 550ba6db7a6c7b72dde28b533838fb3ab4cd19f8 Mon Sep 17 00:00:00 2001 From: Aurelien Campeas Date: Tue, 13 Nov 2012 14:11:33 +0100 Subject: [loggin ext] provide colored output under windows if colorama is available (closes #107436) --- ChangeLog | 3 +++ __pkginfo__.py | 6 +++++- logging_ext.py | 15 +++++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index bffa78b..03d3540 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,9 @@ ChangeLog for logilab.common ============================ +-- + * loggin_ext: on windows, use colorama to display colored logs, if available (closes #107436) + 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 diff --git a/__pkginfo__.py b/__pkginfo__.py index 5f47663..78b7757 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -18,6 +18,7 @@ """logilab.common packaging information""" __docformat__ = "restructuredtext en" import sys +import os distname = 'logilab-common' modname = 'common' @@ -40,8 +41,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/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: -- cgit v1.2.1 From 6bc5b493366d5da247e5b45995b910583db4f230 Mon Sep 17 00:00:00 2001 From: David Douard Date: Wed, 14 Nov 2012 16:04:34 +0100 Subject: [umessage] add a UMessage.__getitem__ method to be closer to email.Message API --- umessage.py | 3 +++ 1 file changed, 3 insertions(+) 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] -- cgit v1.2.1 From 1d0f11913a66d5410c65a9c039a1630218b2e3ae Mon Sep 17 00:00:00 2001 From: David Douard Date: Wed, 14 Nov 2012 16:26:31 +0100 Subject: 0.58.3 --- ChangeLog | 7 ++++++- __pkginfo__.py | 2 +- debian/changelog | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4ce2512..bffa78b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,7 @@ ChangeLog for logilab.common ============================ --- +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) @@ -10,6 +10,11 @@ ChangeLog for logilab.common * registry: 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) diff --git a/__pkginfo__.py b/__pkginfo__.py index 557cb59..5f47663 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -24,7 +24,7 @@ modname = 'common' subpackage_of = 'logilab' subpackage_master = True -numversion = (0, 58, 2) +numversion = (0, 58, 3) version = '.'.join([str(num) for num in numversion]) license = 'LGPL' # 2.1 or later diff --git a/debian/changelog b/debian/changelog index 8b11319..21428ff 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +logilab-common (0.58.3-1) unstable; urgency=low + + * new upstream release + + -- David Douard Wed, 14 Nov 2012 16:18:53 +0100 + logilab-common (0.58.2-1) precise; urgency=low * new upstream release -- cgit v1.2.1 -- cgit v1.2.1 -- cgit v1.2.1 From 154e16348a2833fc5a0647caedac51644f80e5d4 Mon Sep 17 00:00:00 2001 From: Julien Cristau Date: Fri, 16 Nov 2012 16:48:53 +0100 Subject: [packaging] remove references to ftp://ftp.logilab.org It has been replaced by http://download.logilab.org/ --- __pkginfo__.py | 1 - debian/copyright | 4 ++-- debian/watch | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/__pkginfo__.py b/__pkginfo__.py index 78b7757..fc3cb69 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -31,7 +31,6 @@ 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" 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 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 -- cgit v1.2.1 From a268e0b064fbef6f45a6fa3cc73b52fd38891f41 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 30 Nov 2012 11:26:36 +0100 Subject: [test, deprecation] update tests so we actually test something --- deprecation.py | 2 +- test/unittest_deprecation.py | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) 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/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() -- cgit v1.2.1 From 216e5448c2338dfb84a1dfd488cd235ddc22775b Mon Sep 17 00:00:00 2001 From: Julien Cristau Date: Thu, 6 Dec 2012 11:42:31 +0100 Subject: [packaging] steal spec file from fedora (closes #113099) --- python-logilab-common.spec | 168 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 python-logilab-common.spec diff --git a/python-logilab-common.spec b/python-logilab-common.spec new file mode 100644 index 0000000..3f4ebc0 --- /dev/null +++ b/python-logilab-common.spec @@ -0,0 +1,168 @@ +%{!?_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: 1%{?dist} +Summary: Common libraries for Logilab projects + +Group: Development/Libraries +License: GPLv2+ +URL: http://www.logilab.org/projects/common +Source0: ftp://ftp.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 + + +%install +rm -rf $RPM_BUILD_ROOT +%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT +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 Aug 03 2012 Brian C. Lane 0.58.2-1 +- Upstream 0.58.2 + +* Sat Jul 21 2012 Fedora Release Engineering - 0.57.1-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild + +* Sat Jan 14 2012 Fedora Release Engineering - 0.57.1-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_17_Mass_Rebuild + +* Fri Nov 18 2011 Brian C. Lane - 0.57.1-1 +- Upstream 0.57.1 + +* Fri Jul 29 2011 Brian C. Lane - 0.56.0-1 +- Upstream 0.56.0 + +* Mon Mar 28 2011 Brian C. Lane - 0.55.1-1 +- Upstream 0.55.1 +- Add unit tests to spec + +* Tue Feb 08 2011 Fedora Release Engineering - 0.53.0-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild + +* Mon Nov 29 2010 Brian C. Lane - 0.53.0-1 +- Upstream 0.53.0 + +* Thu Jul 22 2010 David Malcolm - 0.50.3-2 +- Rebuilt for https://fedoraproject.org/wiki/Features/Python_2.7/MassRebuild + +* Thu Jul 08 2010 Brian C. Lane - 0.50.3-1 +- Upstream 0.50.3 + +* Fri Mar 26 2010 Brian C. Lane - 0.49.0-2 +- Add python-setuptools to BuildRequires + +* Thu Mar 25 2010 Brian C. Lane - 0.49.0-1 +- Upstream 0.49.0 + +* Sun Aug 30 2009 Konstantin Ryabitsev - 0.45.0-1 +- Upstream 0.45.0 (small enhancements and bugfixes) + +* Sun Jul 26 2009 Fedora Release Engineering - 0.41.0-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_12_Mass_Rebuild + +* Wed Jun 17 2009 Konstantin Ryabitsev - 0.41.0-2 +- Upstream 0.41.0 +- Bugfixes and a few minor new features + +* Thu Feb 26 2009 Fedora Release Engineering - 0.38.0-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_11_Mass_Rebuild + +* Wed Jan 28 2009 Konstantin Ryabitsev - 0.38.0-1 +- Upstream 0.38.0 + +* Tue Dec 30 2008 Konstantin Ryabitsev - 0.37.0-1 +- Upstream 0.37.0 + +* Sat Nov 29 2008 Ignacio Vazquez-Abrams - 0.32.0-2 +- Rebuild for Python 2.6 + +* Mon Jun 30 2008 Konstantin Ryabitsev - 0.32.0-1 +- Upstream 0.32.0 + +* Sun Feb 17 2008 Konstantin Ryabitsev - 0.28.0-1 +- Upstream 0.28.0 + +* Thu Jan 17 2008 Konstantin Ryabitsev - 0.26.1-1 +- Upstream 0.26.1 +- Package egg-info and other files. + +* Mon Dec 24 2007 Konstantin Ryabitsev - 0.25.2-1 +- Upstream 0.25.2 + +* Sun Nov 18 2007 Konstantin Ryabitsev - 0.24.0-1 +- Upstream 0.24.0 +- Adjust license to the new standard + +* Sun Apr 01 2007 Konstantin Ryabitsev - 0.21.2-1 +- Upstream 0.21.2 + +* Sun Dec 17 2006 Konstantin Ryabitsev - 0.21.0-1 +- Upstream 0.21.0 +- Include COPYING with docs + +* Tue Sep 26 2006 Konstantin Ryabitsev - 0.19.2-1 +- Upstream 0.19.2 +- Ghostbusting +- Require mx + +* Mon May 01 2006 Konstantin Ryabitsev - 0.15.0-1 +- Version 0.15.0 + +* Sun Mar 12 2006 Konstantin Ryabitsev - 0.14.1-2 +- Also handle __init__.pyc and __init__.pyo + +* Sun Mar 12 2006 Konstantin Ryabitsev - 0.14.1-1 +- Version 0.14.1 + +* Thu Jan 12 2006 Konstantin Ryabitsev - 0.13.0-1 +- Version 0.13.0 +- astng no longer part of the package + +* Thu Nov 17 2005 Konstantin Ryabitsev - 0.12.0-1 +- Version 0.12.0 + +* Mon Jun 13 2005 Konstantin Ryabitsev - 0.10.0-1 +- Version 0.10.0. +- Disttagging. + +* Thu May 05 2005 Konstantin Ryabitsev - 0.9.3-3 +- Fix paths. + +* Tue Apr 26 2005 Konstantin Ryabitsev - 0.9.3-2 +- Ghost .pyo files. +- Get rid of test, which doesn't do anything. + +* Fri Apr 22 2005 Konstantin Ryabitsev - 0.9.3-1 +- Initial packaging. -- cgit v1.2.1 From d83cd26694532036c640787f74c0ff6b81a4da00 Mon Sep 17 00:00:00 2001 From: Julien Cristau Date: Thu, 6 Dec 2012 12:01:05 +0100 Subject: [packaging] force python2.6 on rhel5 (closes #113099) --- python-logilab-common.spec | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/python-logilab-common.spec b/python-logilab-common.spec index 3f4ebc0..2f6afe1 100644 --- a/python-logilab-common.spec +++ b/python-logilab-common.spec @@ -1,8 +1,17 @@ +# 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 +Name: %{python}-logilab-common Version: 0.58.2 -Release: 1%{?dist} +Release: logilab.1%{?dist} Summary: Common libraries for Logilab projects Group: Development/Libraries @@ -27,11 +36,15 @@ shared among some python projects developed by logilab. %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 -%{__python} setup.py install -O1 --skip-build --root $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 @@ -49,6 +62,9 @@ rm -rf $RPM_BUILD_ROOT %changelog +* Fri Nov 16 2012 Julien Cristau 0.58.2-logilab.1 +- Force python26 on el5 + * Fri Aug 03 2012 Brian C. Lane 0.58.2-1 - Upstream 0.58.2 -- cgit v1.2.1 From a1a73d8548c845536d15e849513949cf0f7ebae2 Mon Sep 17 00:00:00 2001 From: Julien Cristau Date: Thu, 6 Dec 2012 12:00:48 +0100 Subject: [packaging] Update download and project urls (closes #113099) --- python-logilab-common.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python-logilab-common.spec b/python-logilab-common.spec index 2f6afe1..0ee1282 100644 --- a/python-logilab-common.spec +++ b/python-logilab-common.spec @@ -16,8 +16,8 @@ Summary: Common libraries for Logilab projects Group: Development/Libraries License: GPLv2+ -URL: http://www.logilab.org/projects/common -Source0: ftp://ftp.logilab.org/pub/common/logilab-common-%{version}.tar.gz +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) -- cgit v1.2.1 From 0c2331cf53047d60f6507d416e13d5c00395ecf2 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Thu, 13 Dec 2012 14:33:53 +0100 Subject: [configuration] enhance merge_options function: copy option dictionaries and allow to ensure all options are in the same group using optional optgroup argument. Closes #113458 --- configuration.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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) -- cgit v1.2.1 From 247b86271595f52cecfd4c998442f6e5c5ae7014 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 11 Jan 2013 12:44:55 +0100 Subject: [registry] introduce objid/objname method on registries instead of using function/inlined code. Partially closes #98742 So they become customizable per registry. --- ChangeLog | 4 ++++ registry.py | 50 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/ChangeLog b/ChangeLog index 03d3540..d642350 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,10 @@ ChangeLog for logilab.common ============================ -- + * registry: + - introduce objid and objname methods on Registry instead of classid + function and inlined code (closes #98742) + * loggin_ext: on windows, use colorama to display colored logs, if available (closes #107436) 2012-11-14 -- 0.58.3 diff --git a/registry.py b/registry.py index 86065c5..05b8b2e 100644 --- a/registry.py +++ b/registry.py @@ -145,10 +145,6 @@ def _toload_info(path, extrapath, _toload=None): return _toload -def classid(cls): - """returns a unique identifier for an object class""" - return '%s.%s' % (cls.__module__, cls.__name__) - def class_registries(cls, registryname): """return a tuple of registry names (see __registries__)""" if registryname: @@ -202,6 +198,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""" @@ -234,12 +240,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: @@ -249,17 +255,17 @@ class Registry(dict): def unregister(self, obj): """remove object 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. @@ -550,8 +556,8 @@ class RegistryStore(dict): 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 + 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 @@ -656,9 +662,7 @@ class RegistryStore(dict): if hasattr(module, 'registration_callback'): module.registration_callback(self) else: - for objname, obj in vars(module).items(): - if objname.startswith('_'): - continue + for obj in vars(module).values(): self._load_ancestors_then_object(module.__name__, obj) def _load_ancestors_then_object(self, modname, objectcls): @@ -685,7 +689,8 @@ class RegistryStore(dict): return except TypeError: return - clsid = classid(objectcls) + reg = self.setdefault(class_registries(obj)[0]) + clsid = reg.objid(obj) if clsid in self._loadedmods[modname]: return self._loadedmods[modname][clsid] = objectcls @@ -974,3 +979,14 @@ 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): + """returns a unique identifier for an object class""" + return '%s.%s' % (cls.__module__, cls.__name__) + -- cgit v1.2.1 From 942ef514462522c0ea250fbc402e5ae67fbeaf5e Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 11 Jan 2013 12:07:10 +0100 Subject: [registry]?deprecate usage of leading underscore to mark class as abstract / not to register --- ChangeLog | 3 +++ registry.py | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index d642350..ac74ea0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,9 @@ ChangeLog for logilab.common * registry: - introduce objid and objname methods on Registry instead of classid function and inlined code (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 * loggin_ext: on windows, use colorama to display colored logs, if available (closes #107436) diff --git a/registry.py b/registry.py index 05b8b2e..7a4072c 100644 --- a/registry.py +++ b/registry.py @@ -670,8 +670,7 @@ class RegistryStore(dict): - 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 + - class with __abstract__ == True in their local dictionary are skipped - object class needs to have __registry__ and __regid__ attributes set to a non empty string to be registered. @@ -696,8 +695,13 @@ class RegistryStore(dict): self._loadedmods[modname][clsid] = objectcls for parent in objectcls.__bases__: self._load_ancestors_then_object(modname, parent) + if reg.objname(obj)[0] == '_': + warn("[lgc 0.59] object whose name start with '_' won't be " + "skipped anymore at some point, use __abstract__ = True " + "instead (%s)" % obj, DeprecationWarning) + return + if (objectcls.__dict__.get('__abstract__') - or objectcls.__name__[0] == '_' or not objectcls.__registries__ or not objectcls.__regid__): return -- cgit v1.2.1 From 2ade5840f06b1df041bcb2a081acaa5b3ec572ab Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 11 Jan 2013 12:07:31 +0100 Subject: minor change to changelog --- ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index ac74ea0..1d00693 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,7 +18,7 @@ ChangeLog for logilab.common * other python3 code and test fixes (closes #104047) - * registry: setdefault shouldn't raise RegistryNotFound (closes #111010) + * registry: Store.setdefault shouldn't raise RegistryNotFound (closes #111010) * table: stop encoding to iso-8859-1, use unicode (closes #105847) -- cgit v1.2.1 From 43654849e3ffaddf7990c5f904ec54445b193dca Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 21 Dec 2012 11:03:49 +0100 Subject: [registry] enhanced logging during registration --- registry.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/registry.py b/registry.py index 7a4072c..cce2f3f 100644 --- a/registry.py +++ b/registry.py @@ -221,8 +221,8 @@ 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, ('no explicit name supplied to register object %s, ' 'which has no __regid__ set' % obj) @@ -230,8 +230,7 @@ class Registry(dict): 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): @@ -546,7 +545,7 @@ class RegistryStore(dict): If `clear` is true, all objects with the same identifier will be previously unregistered. """ - assert not obj.__dict__.get('__abstract__') + assert not obj.__dict__.get('__abstract__'), obj try: vname = obj.__name__ except AttributeError: @@ -555,7 +554,7 @@ class RegistryStore(dict): for registryname in class_registries(obj, registryname): registry = self.setdefault(registryname) registry.register(obj, oid=oid, clear=clear) - self.debug('register %s in %s[\'%s\']', + self.debug("register %s in %s['%s']", registry.objname(obj), registryname, oid or obj.__regid__) self._loadedmods.setdefault(obj.__module__, {})[registry.objid(obj)] = obj @@ -564,7 +563,10 @@ class RegistryStore(dict): `registryname` or `obj.__registry__` if not specified. """ for registryname in class_registries(obj, registryname): - self[registryname].unregister(obj) + 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 @@ -572,8 +574,12 @@ class RegistryStore(dict): will be unregistered first (else a warning will be issued as it's 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 ################################################### -- cgit v1.2.1 From b2c601c590f1b54916ea8eab8280b0118c1a7e6a Mon Sep 17 00:00:00 2001 From: Aurelien Campeas Date: Mon, 14 Jan 2013 11:19:16 +0100 Subject: [registry] cleanup the doc and add some comments --- registry.py | 113 ++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 61 insertions(+), 52 deletions(-) diff --git a/registry.py b/registry.py index cce2f3f..49614db 100644 --- a/registry.py +++ b/registry.py @@ -353,13 +353,14 @@ class Registry(dict): 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'...) @@ -367,28 +368,29 @@ class RegistryStore(dict): registries :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) @@ -397,22 +399,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 @@ -423,51 +428,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): @@ -511,7 +513,7 @@ class RegistryStore(dict): return self[regid] def register_all(self, objects, modname, butclasses=()): - """register all `objects` given. Objects which are not from the module + """register all given `objects`. Objects which are not from the module `modname` or which are in `butclasses` won't be registered. Typical usage is: @@ -559,8 +561,8 @@ class RegistryStore(dict): 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.__registry__` if not specified. """ for registryname in class_registries(obj, registryname): registry = self[registryname] @@ -569,9 +571,9 @@ class RegistryStore(dict): registry.objname(obj), registryname, obj.__regid__) def register_and_replace(self, obj, replaced, registryname=None): - """register `obj` implementation object into `registryname` or + """register `obj` 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 + will be unregistered first (else a warning will be issued as it is generally unexpected). """ for registryname in obj_registries(obj, registryname): @@ -611,6 +613,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: @@ -642,7 +645,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 @@ -684,7 +687,12 @@ class RegistryStore(dict): # imported classes objmodname = getattr(objectcls, '__module__', None) 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 @@ -711,6 +719,7 @@ class RegistryStore(dict): or not objectcls.__registries__ or not objectcls.__regid__): return + try: self.register(objectcls) except Exception, ex: @@ -766,7 +775,7 @@ class traced_selection(object): # pylint: disable=C0103 This will yield lines like this in the logs:: - selector one_line_rset returned 0 for + selector one_line_rset returned 0 for 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). -- cgit v1.2.1 From c0308a32ff35d921e75c5cff68df14aeacd4b9f9 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Jan 2013 14:08:48 +0100 Subject: [registry] introduce RegistrableObject and RegistrableInstance base classes. Closes #98742 and make them mandatory *for automatic registration*. Cleanup automatic registration code accordingly. Instances are now registrable, and automatically registered provided they inherit from RegistrableInstance. --- ChangeLog | 32 +++--- registry.py | 259 ++++++++++++++++++++++++++++++++-------------- test/data/regobjects.py | 22 ++++ test/data/regobjects2.py | 8 ++ test/unittest_registry.py | 44 +++++++- 5 files changed, 271 insertions(+), 94 deletions(-) create mode 100644 test/data/regobjects.py create mode 100644 test/data/regobjects2.py diff --git a/ChangeLog b/ChangeLog index 1d00693..61c8b48 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,8 +3,16 @@ ChangeLog for logilab.common -- * registry: - - introduce objid and objname methods on Registry instead of classid - function and inlined code (closes #98742) + + - 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 @@ -312,7 +320,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 @@ -326,9 +334,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 @@ -501,7 +509,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) @@ -652,7 +660,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 @@ -701,7 +709,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 @@ -773,7 +781,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: @@ -1002,8 +1010,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/registry.py b/registry.py index 49614db..7876085 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 : 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,31 +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 class_registries(cls, registryname): - """return a tuple of registry names (see __registries__)""" - if registryname: - return (registryname,) - return cls.__registries__ +class RegistrableObject(object): + """This is the base class for registrable objects which are selected + according to a context. + + :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): @@ -345,13 +397,25 @@ 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 objects and storing them in their registry which is created on the fly as needed. @@ -362,10 +426,9 @@ class RegistryStore(dict): (: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__` object identifier in the registry (string like 'main', @@ -513,7 +576,10 @@ class RegistryStore(dict): return self[regid] def register_all(self, objects, modname, butclasses=()): - """register all given `objects`. 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: @@ -524,36 +590,27 @@ 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 + if self.is_registrable(obj) and obj.__module__ == modname and not obj in butclasses: oid = obj.__regid__ - except AttributeError: - continue - if oid and not obj.__dict__.get('__abstract__'): - self.register(obj, oid=oid) + if oid and not obj.__dict__.get('__abstract__'): + self.register(obj, oid=oid) 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__'), obj - try: - vname = obj.__name__ - except AttributeError: - # XXX may occurs? - vname = obj.__class__.__name__ - for registryname in class_registries(obj, registryname): + 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']", @@ -562,9 +619,9 @@ class RegistryStore(dict): def unregister(self, obj, registryname=None): """unregister `obj` object from the registry `registryname` or - `obj.__registry__` if not specified. + `obj.__registries__` if not specified. """ - for registryname in class_registries(obj, registryname): + for registryname in obj_registries(obj, registryname): registry = self[registryname] registry.unregister(obj) self.debug("unregister %s from %s['%s']", @@ -572,7 +629,7 @@ class RegistryStore(dict): def register_and_replace(self, obj, replaced, registryname=None): """register `obj` object into `registryname` or - `obj.__registry__` if not specified. If found, the `replaced` object + `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). """ @@ -665,27 +722,41 @@ 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 obj in vars(module).values(): - self._load_ancestors_then_object(module.__name__, obj) + if self.is_registrable(obj) and obj.__module__ == module.__name__: + if isinstance(obj, type): + self._load_ancestors_then_object(module.__name__, obj) + else: + self.register(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 are skipped - - - object class needs to have __registry__ and __regid__ attributes - set to a non empty string to be registered. + """handle class registration according to rules defined in + :meth:`load_module` """ # 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 @@ -695,38 +766,63 @@ class RegistryStore(dict): # 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 - reg = self.setdefault(class_registries(obj)[0]) - clsid = reg.objid(obj) + # ensure object hasn't been already processed + clsid = '%s.%s' % (objmodname, 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 reg.objname(obj)[0] == '_': + # ensure object is registrable + if not self.is_registrable(objectcls): + return + # 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)" % obj, DeprecationWarning) + "instead (%s)" % objectcls, DeprecationWarning) return + # register, finally + self.register(objectcls) - if (objectcls.__dict__.get('__abstract__') - or not objectcls.__registries__ - or not objectcls.__regid__): - return + @classmethod + def is_registrable(cls, obj): + """ensure `obj` should be registered - try: - self.register(objectcls) - except Exception, ex: - if self.debugmode: - raise - self.exception('object %s registration failed: %s', - objectcls, ex) + 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 @@ -1006,6 +1102,9 @@ from logilab.common.deprecation import deprecated @deprecated('[lgc 0.59] use Registry.objid class method instead') def classid(cls): - """returns a unique identifier for an object class""" 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_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() -- cgit v1.2.1 From f2651f0c26d6716173b05dbc38fb89e79d2d4781 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Wed, 9 Jan 2013 11:18:05 +0100 Subject: [registry]?use register_all when no registration callback defined. Closes #111011 --- ChangeLog | 2 ++ registry.py | 29 +++++++++++++++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/ChangeLog b/ChangeLog index 61c8b48..db6ba78 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,8 @@ ChangeLog for logilab.common __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) 2012-11-14 -- 0.58.3 diff --git a/registry.py b/registry.py index 7876085..fec35ad 100644 --- a/registry.py +++ b/registry.py @@ -597,9 +597,10 @@ class RegistryStore(dict): 'modname expected to be a module name (ie string), got %r' % modname for obj in objects: if self.is_registrable(obj) and obj.__module__ == modname and not obj in butclasses: - oid = obj.__regid__ - if oid and not obj.__dict__.get('__abstract__'): - self.register(obj, oid=oid) + 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 @@ -744,17 +745,17 @@ class RegistryStore(dict): if hasattr(module, 'registration_callback'): module.registration_callback(self) else: - for obj in vars(module).values(): - if self.is_registrable(obj) and obj.__module__ == module.__name__: - if isinstance(obj, type): - self._load_ancestors_then_object(module.__name__, obj) - else: - self.register(obj) - - def _load_ancestors_then_object(self, modname, objectcls): + self.register_all(vars(module).itervalues(), module.__name__) + + 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 = objectcls.__module__ if objmodname != modname: @@ -767,15 +768,15 @@ class RegistryStore(dict): self.load_file(self._toloadmods[objmodname], objmodname) return # ensure object hasn't been already processed - clsid = '%s.%s' % (objmodname, objectcls.__name__) + 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) + self._load_ancestors_then_object(modname, parent, butclasses) # ensure object is registrable - if not self.is_registrable(objectcls): + if objectcls in butclasses or not self.is_registrable(objectcls): return # backward compat reg = self.setdefault(obj_registries(objectcls)[0]) -- cgit v1.2.1 From 20180f3add21b4dc93d728754a1a491793837709 Mon Sep 17 00:00:00 2001 From: Aurelien Campeas Date: Mon, 21 Jan 2013 15:28:36 +0100 Subject: prepare 0.59.0 --- ChangeLog | 18 +++++++++++++++++- __pkginfo__.py | 2 +- debian/changelog | 6 ++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index db6ba78..c579d02 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,9 @@ ChangeLog for logilab.common ============================ --- + +2013-01-21 -- 0.59.0 + * registry: - introduce RegistrableObject base class, mandatory to make @@ -21,6 +23,20 @@ ChangeLog for logilab.common * 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 diff --git a/__pkginfo__.py b/__pkginfo__.py index fc3cb69..3626c34 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -25,7 +25,7 @@ modname = 'common' subpackage_of = 'logilab' subpackage_master = True -numversion = (0, 58, 3) +numversion = (0, 59, 0) version = '.'.join([str(num) for num in numversion]) license = 'LGPL' # 2.1 or later diff --git a/debian/changelog b/debian/changelog index 21428ff..6a23e41 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +logilab-common (0.59.0-1) unstable; urgency=low + + * new upstream release + + -- Aurélien Campéas Tue, 18 Dec 2012 12:35:00 +0100 + logilab-common (0.58.3-1) unstable; urgency=low * new upstream release -- cgit v1.2.1