summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAurelien Campeas <aurelien.campeas@logilab.fr>2013-01-21 15:28:58 +0100
committerAurelien Campeas <aurelien.campeas@logilab.fr>2013-01-21 15:28:58 +0100
commitf73583632f36f0858b1932da3c40cc4c2d0b2b00 (patch)
tree219e872385dbb491be430962a2146738b9424513
parent5fab8339edc52e3ea454e287cc1ac7eec8a20979 (diff)
parent20180f3add21b4dc93d728754a1a491793837709 (diff)
downloadlogilab-common-f73583632f36f0858b1932da3c40cc4c2d0b2b00.tar.gz
[merge] default is stable
-rw-r--r--ChangeLog66
-rw-r--r--__pkginfo__.py9
-rw-r--r--configuration.py15
-rw-r--r--debian/changelog12
-rw-r--r--debian/copyright4
-rw-r--r--debian/watch2
-rw-r--r--deprecation.py2
-rw-r--r--logging_ext.py15
-rw-r--r--python-logilab-common.spec184
-rw-r--r--registry.py448
-rw-r--r--test/data/regobjects.py22
-rw-r--r--test/data/regobjects2.py8
-rw-r--r--test/unittest_deprecation.py22
-rw-r--r--test/unittest_registry.py44
-rw-r--r--umessage.py3
15 files changed, 671 insertions, 185 deletions
diff --git a/ChangeLog b/ChangeLog
index 7ce11d3..c579d02 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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]