Commit d9b9886b authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[selectors] provide a new, optimized, is_instance selector that should at some...

[selectors] provide a new, optimized, is_instance selector that should at some point replace implements (along with the adaptable selector)
parent e77aa963fb19
......@@ -15,12 +15,10 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""only for unit tests !
"""
"""only for unit tests !"""
from cubicweb.view import EntityView
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
HTML_PAGE = u"""<html>
<body>
......@@ -31,7 +29,7 @@ HTML_PAGE = u"""<html>
class SimpleView(EntityView):
__regid__ = 'simple'
__select__ = implements('Bug',)
__select__ = is_instance('Bug',)
def call(self, **kwargs):
self.cell_call(0, 0)
......@@ -41,7 +39,7 @@ class SimpleView(EntityView):
class RaisingView(EntityView):
__regid__ = 'raising'
__select__ = implements('Bug',)
__select__ = is_instance('Bug',)
def cell_call(self, row, col):
raise ValueError()
......@@ -28,7 +28,7 @@ from logilab.mtconverter import TransformError
from logilab.common.decorators import cached
from cubicweb.view import EntityAdapter, implements_adapter_compat
from cubicweb.selectors import implements, relation_possible
from cubicweb.selectors import implements, is_instance, relation_possible
from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone
......@@ -67,7 +67,7 @@ class IEmailableAdapter(EntityAdapter):
class INotifiableAdapter(EntityAdapter):
__regid__ = 'INotifiable'
__select__ = implements('Any')
__select__ = is_instance('Any')
@implements_adapter_compat('INotifiableAdapter')
def notification_references(self, view):
......@@ -85,7 +85,7 @@ class INotifiableAdapter(EntityAdapter):
class IFTIndexableAdapter(EntityAdapter):
__regid__ = 'IFTIndexable'
__select__ = implements('Any')
__select__ = is_instance('Any')
def fti_containers(self, _done=None):
if _done is None:
......
......@@ -27,7 +27,7 @@ from yams.schema import role_name
from cubicweb import ValidationError
from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.uilib import soup2xhtml
from cubicweb.server import hook
from cubicweb.server.hook import set_operation
......@@ -253,7 +253,7 @@ class DontRemoveOwnersGroupHook(IntegrityHook):
"""delete the composed of a composite relation when this relation is deleted
"""
__regid__ = 'checkownersgroup'
__select__ = IntegrityHook.__select__ & implements('CWGroup')
__select__ = IntegrityHook.__select__ & is_instance('CWGroup')
events = ('before_delete_entity', 'before_update_entity')
def __call__(self):
......@@ -293,7 +293,7 @@ class TidyHtmlFields(IntegrityHook):
class StripCWUserLoginHook(IntegrityHook):
"""ensure user logins are stripped"""
__regid__ = 'stripuserlogin'
__select__ = IntegrityHook.__select__ & implements('CWUser')
__select__ = IntegrityHook.__select__ & is_instance('CWUser')
events = ('before_add_entity', 'before_update_entity',)
def __call__(self):
......
......@@ -21,7 +21,7 @@ __docformat__ = "restructuredtext en"
from datetime import datetime
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.server import hook
from cubicweb.server.utils import eschema_eid
......@@ -140,7 +140,7 @@ class SyncCompositeOwner(MetaDataHook):
class FixUserOwnershipHook(MetaDataHook):
"""when a user has been created, add owned_by relation on itself"""
__regid__ = 'fixuserowner'
__select__ = MetaDataHook.__select__ & implements('CWUser')
__select__ = MetaDataHook.__select__ & is_instance('CWUser')
events = ('after_add_entity',)
def __call__(self):
......
......@@ -22,7 +22,7 @@ __docformat__ = "restructuredtext en"
from logilab.common.textutils import normalize_text
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.server import hook
from cubicweb.sobjects.supervising import SupervisionMailOp
......@@ -49,7 +49,7 @@ class NotificationHook(hook.Hook):
class StatusChangeHook(NotificationHook):
"""notify when a workflowable entity has its state modified"""
__regid__ = 'notifystatuschange'
__select__ = NotificationHook.__select__ & implements('TrInfo')
__select__ = NotificationHook.__select__ & is_instance('TrInfo')
events = ('after_add_entity',)
def __call__(self):
......
......@@ -33,7 +33,7 @@ from logilab.common.decorators import clear_cache
from logilab.common.testlib import mock_object
from cubicweb import ValidationError
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS,
ETYPE_NAME_MAP, display_name)
from cubicweb.server import hook, schemaserial as ss
......@@ -809,7 +809,7 @@ class DelCWETypeHook(SyncSchemaHook):
* instantiate an operation to delete the entity type on commit
"""
__regid__ = 'syncdelcwetype'
__select__ = SyncSchemaHook.__select__ & implements('CWEType')
__select__ = SyncSchemaHook.__select__ & is_instance('CWEType')
events = ('before_delete_entity',)
def __call__(self):
......@@ -914,7 +914,7 @@ class DelCWRTypeHook(SyncSchemaHook):
* instantiate an operation to delete the relation type on commit
"""
__regid__ = 'syncdelcwrtype'
__select__ = SyncSchemaHook.__select__ & implements('CWRType')
__select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
events = ('before_delete_entity',)
def __call__(self):
......@@ -1032,7 +1032,7 @@ class AfterDelRelationTypeHook(SyncSchemaHook):
class AfterAddCWAttributeHook(SyncSchemaHook):
__regid__ = 'syncaddcwattribute'
__select__ = SyncSchemaHook.__select__ & implements('CWAttribute')
__select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute')
events = ('after_add_entity',)
def __call__(self):
......@@ -1041,7 +1041,7 @@ class AfterAddCWAttributeHook(SyncSchemaHook):
class AfterAddCWRelationHook(AfterAddCWAttributeHook):
__regid__ = 'syncaddcwrelation'
__select__ = SyncSchemaHook.__select__ & implements('CWRelation')
__select__ = SyncSchemaHook.__select__ & is_instance('CWRelation')
def __call__(self):
SourceDbCWRelationAdd(self._cw, entity=self.entity)
......@@ -1049,7 +1049,7 @@ class AfterAddCWRelationHook(AfterAddCWAttributeHook):
class AfterUpdateCWRDefHook(SyncSchemaHook):
__regid__ = 'syncaddcwattribute'
__select__ = SyncSchemaHook.__select__ & implements('CWAttribute',
__select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute',
'CWRelation')
events = ('before_update_entity',)
......@@ -1081,7 +1081,7 @@ class AfterUpdateCWRDefHook(SyncSchemaHook):
class AfterAddCWConstraintHook(SyncSchemaHook):
__regid__ = 'syncaddcwconstraint'
__select__ = SyncSchemaHook.__select__ & implements('CWConstraint')
__select__ = SyncSchemaHook.__select__ & is_instance('CWConstraint')
events = ('after_add_entity', 'after_update_entity')
def __call__(self):
......
......@@ -22,7 +22,7 @@ __docformat__ = "restructuredtext en"
from yams.schema import role_name
from cubicweb import UnknownProperty, ValidationError, BadConnectionId
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.server import hook
......@@ -108,7 +108,7 @@ class _DelUserOp(hook.Operation):
class CloseDeletedUserSessionsHook(SyncSessionHook):
__regid__ = 'closession'
__select__ = SyncSessionHook.__select__ & implements('CWUser')
__select__ = SyncSessionHook.__select__ & is_instance('CWUser')
events = ('after_delete_entity',)
def __call__(self):
......@@ -152,7 +152,7 @@ class _AddCWPropertyOp(hook.Operation):
class AddCWPropertyHook(SyncSessionHook):
__regid__ = 'addcwprop'
__select__ = SyncSessionHook.__select__ & implements('CWProperty')
__select__ = SyncSessionHook.__select__ & is_instance('CWProperty')
events = ('after_add_entity',)
def __call__(self):
......
......@@ -24,7 +24,7 @@ from datetime import datetime
from yams.schema import role_name
from cubicweb import RepositoryError, ValidationError
from cubicweb.selectors import implements, adaptable
from cubicweb.selectors import is_instance, adaptable
from cubicweb.server import hook
......@@ -177,7 +177,7 @@ class FireTransitionHook(WorkflowHook):
* by_transition or to_state (managers only) inlined relation is set
"""
__regid__ = 'wffiretransition'
__select__ = WorkflowHook.__select__ & implements('TrInfo')
__select__ = WorkflowHook.__select__ & is_instance('TrInfo')
events = ('before_add_entity',)
def __call__(self):
......@@ -273,7 +273,7 @@ class FireTransitionHook(WorkflowHook):
class FiredTransitionHook(WorkflowHook):
"""change related entity state"""
__regid__ = 'wffiretransition'
__select__ = WorkflowHook.__select__ & implements('TrInfo')
__select__ = WorkflowHook.__select__ & is_instance('TrInfo')
events = ('after_add_entity',)
def __call__(self):
......
......@@ -244,31 +244,6 @@ class PartialSelectorMixIn(object):
return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
class ImplementsMixIn(object):
"""mix-in class for selectors checking implemented interfaces of something
"""
def __init__(self, *expected_ifaces, **kwargs):
super(ImplementsMixIn, self).__init__(**kwargs)
self.expected_ifaces = expected_ifaces
def __str__(self):
return '%s(%s)' % (self.__class__.__name__,
','.join(str(s) for s in self.expected_ifaces))
def score_interfaces(self, req, cls_or_inst, cls):
score = 0
etypesreg = req.vreg['etypes']
for iface in self.expected_ifaces:
if isinstance(iface, basestring):
# entity type
try:
iface = etypesreg.etype_class(iface)
except KeyError:
continue # entity type not in the schema
score += score_interface(etypesreg, cls_or_inst, cls, iface)
return score
class EClassSelector(Selector):
"""abstract class for selectors working on *entity class(es)* specified
explicitly or found of the result set.
......@@ -411,7 +386,7 @@ class ExpectedValueSelector(Selector):
"""Take a list of expected values as initializer argument and store them
into the :attr:`expected` set attribute.
You should implements the :meth:`_get_value(cls, req, **kwargs)` method
You should implement the :meth:`_get_value(cls, req, **kwargs)` method
which should return the value for the given context. The selector will then
return 1 if the value is expected, else 0.
"""
......@@ -484,8 +459,8 @@ class adaptable(appobject_selectable):
(usually entities) should be adaptable. One of them should be selectable
when multiple identifiers are given.
"""
# implementing an interface takes precedence other special Any interface,
# hence return 2 (implements('Any') score is 1)
# being adaptable to an interface takes precedence other is_instance('Any'),
# hence return 2 (is_instance('Any') score is 1)
selectable_score = 2
def __init__(self, *regids):
super(adaptable, self).__init__('adapters', *regids)
......@@ -664,7 +639,7 @@ def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs):
class non_final_entity(EClassSelector):
"""Return 1 for entity of a non final entity type(s). Remember, "final"
entity types are String, Int, etc... This is equivalent to
`implements('Any')` but more optimized.
`is_instance('Any')` but more optimized.
See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
class lookup / score rules according to the input context.
......@@ -678,7 +653,7 @@ class non_final_entity(EClassSelector):
return 1 # necessarily true if we're there
class implements(ImplementsMixIn, EClassSelector):
class implements(EClassSelector):
"""Return non-zero score for entity that are of the given type(s) or
implements at least one of the given interface(s). If multiple arguments are
given, matching one of them is enough.
......@@ -692,14 +667,100 @@ class implements(ImplementsMixIn, EClassSelector):
.. note:: when interface is an entity class, the score will reflect class
proximity so the most specific object will be selected.
.. note:: with cubicweb >= 3.9, you should use adapters instead of
interface, so no interface should be given to this selector. Use
:class:`adaptable` instead.
.. note:: deprecated in cubicweb >= 3.9, use either
:class:`~cubicweb.selectors.is_instance` or
:class:`~cubicweb.selectors.adaptable`.
"""
def __init__(self, *expected_ifaces, **kwargs):
super(implements, self).__init__(**kwargs)
self.expected_ifaces = expected_ifaces
warn('[3.9] implements selector is deprecated, use either is_instance '
'or adaptable', DeprecationWarning, stacklevel=1)
def __str__(self):
return '%s(%s)' % (self.__class__.__name__,
','.join(str(s) for s in self.expected_ifaces))
def score_class(self, eclass, req):
return self.score_interfaces(req, eclass, eclass)
def score_interfaces(self, req, cls_or_inst, cls):
score = 0
etypesreg = req.vreg['etypes']
for iface in self.expected_ifaces:
if isinstance(iface, basestring):
# entity type
try:
iface = etypesreg.etype_class(iface)
except KeyError:
continue # entity type not in the schema
score += score_interface(etypesreg, cls_or_inst, cls, iface)
return score
class is_instance(EClassSelector):
"""Return non-zero score for entity that is an instance of the one of given
type(s). If multiple arguments are given, matching one of them is enough.
Entity types should be given as string, the corresponding class will be
fetched from the registry at selection time.
See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
class lookup / score rules according to the input context.
.. note:: the score will reflect class proximity so the most specific object
will be selected.
"""
def __init__(self, *expected_etypes, **kwargs):
super(is_instance, self).__init__(**kwargs)
self.expected_etypes = expected_etypes
for etype in self.expected_etypes:
assert isinstance(etype, basestring), etype
def __str__(self):
return '%s(%s)' % (self.__class__.__name__,
','.join(str(s) for s in self.expected_etypes))
def score_class(self, eclass, req):
return self.score_etypes(req, eclass, eclass)
def score_etypes(self, req, cls_or_inst, cls):
# cache on vreg to avoid reloading issues
try:
cache = req.vreg.__is_instance_cache
except AttributeError:
cache = req.vreg.__is_instance_cache = {}
try:
expected_eclasses = cache[self]
except KeyError:
# turn list of entity types as string into a list of
# (entity class, parent classes)
etypesreg = req.vreg['etypes']
expected_eclasses = cache[self] = []
for etype in self.expected_etypes:
try:
expected_eclasses.append(
(etypesreg.etype_class(etype),
etypesreg.parent_classes(etype))
)
except KeyError:
continue # entity type not in the schema
score = 0
for iface, parents in expected_eclasses:
# adjust score according to class proximity
if iface is cls:
score += len(parents) + 4
elif iface is parents[-1]: # Any
score += 1
else:
for index, basecls in enumerate(reversed(parents[:-1])):
if iface is basecls:
score += index + 3
break
return score
class score_entity(EntitySelector):
"""Return score according to an arbitrary function given as argument which
......@@ -1202,18 +1263,15 @@ class match_form_params(ExpectedValueSelector):
return len(self.expected)
class specified_etype_implements(implements):
class specified_etype_implements(is_instance):
"""Return non-zero score if the entity type specified by an 'etype' key
searched in (by priority) input context kwargs and request form parameters
match a known entity type (case insensitivly), and it's associated entity
class is of one of the type(s) given to the initializer or implements at
least one of the given interfaces. If multiple arguments are given, matching
one of them is enough.
class is of one of the type(s) given to the initializer. If multiple
arguments are given, matching one of them is enough.
Entity types should be given as string, the corresponding class will be
fetched from the entity types registry at selection time.
.. note:: when interface is an entity class, the score will reflect class
.. note:: as with :class:`~cubicweb.selectors.is_instance`, entity types
should be given as string and the score will reflect class
proximity so the most specific object will be selected.
This selector is usually used by views holding entity creation forms (since
......@@ -1292,7 +1350,7 @@ def debug_mode(cls, req, rset=None, **kwargs):
## deprecated stuff ############################################################
entity_implements = class_renamed('entity_implements', implements)
entity_implements = class_renamed('entity_implements', is_instance)
class _but_etype(EntitySelector):
"""accept if the given entity types are not found in the result set.
......@@ -1310,7 +1368,7 @@ class _but_etype(EntitySelector):
return 0
return 1
but_etype = class_renamed('but_etype', _but_etype, 'use ~implements(*etypes) instead')
but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
# XXX deprecated the one_* variants of selectors below w/ multi_xxx(nb=1)?
......
......@@ -63,7 +63,7 @@ from logilab.common.logging_ext import set_log_methods
from cubicweb import RegistryNotFound
from cubicweb.cwvreg import CWRegistry, VRegistry
from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
implements)
is_instance)
from cubicweb.appobject import AppObject
from cubicweb.server.session import security_enabled
......@@ -246,7 +246,7 @@ class Hook(AppObject):
if ertype.islower():
rtypes.append(ertype)
else:
cls.__select__ = cls.__select__ & implements(ertype)
cls.__select__ = cls.__select__ & is_instance(ertype)
if rtypes:
cls.__select__ = cls.__select__ & match_rtype(*rtypes)
return cls
......@@ -262,7 +262,7 @@ class Hook(AppObject):
def __call__(self):
if hasattr(self, 'call'):
cls = self.__class__
warn('[3.6] %s.%s: call is deprecated, implements __call__'
warn('[3.6] %s.%s: call is deprecated, implement __call__'
% (cls.__module__, cls.__name__), DeprecationWarning)
if self.event.endswith('_relation'):
self.call(self._cw, self.eidfrom, self.rtype, self.eidto)
......
......@@ -2,7 +2,7 @@ from __future__ import with_statement
from cubicweb.devtools import ApptestConfiguration
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.entities.adapters import IFTIndexableAdapter
class PostgresFTITC(CubicWebTC):
......@@ -23,7 +23,7 @@ class PostgresFTITC(CubicWebTC):
def test_attr_weight(self):
class CardIFTIndexableAdapter(IFTIndexableAdapter):
__select__ = implements('Card')
__select__ = is_instance('Card')
attr_weight = {'title': 'A'}
with self.temporary_appobjects(CardIFTIndexableAdapter):
req = self.request()
......@@ -40,7 +40,7 @@ class PostgresFTITC(CubicWebTC):
def test_entity_weight(self):
class PersonneIFTIndexableAdapter(IFTIndexableAdapter):
__select__ = implements('Personne')
__select__ = is_instance('Personne')
entity_weight = 2.0
with self.temporary_appobjects(PersonneIFTIndexableAdapter):
req = self.request()
......
......@@ -25,7 +25,6 @@ from logilab.common.testlib import TestCase, unittest_main, mock_object
from cubicweb.devtools import TestServerConfiguration
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.selectors import implements
from cubicweb.server import hook
from cubicweb.hooks import integrity, syncschema
......
......@@ -33,7 +33,7 @@ from yams.constraints import UniqueConstraint
from cubicweb import (BadConnectionId, RepositoryError, ValidationError,
UnknownEid, AuthenticationError)
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.schema import CubicWebSchema, RQLConstraint
from cubicweb.dbapi import connect, multiple_connections_unfix
from cubicweb.devtools.testlib import CubicWebTC
......@@ -387,7 +387,7 @@ class RepositoryTC(CubicWebTC):
# local hook
class DummyBeforeHook(Hook):
__regid__ = 'dummy-before-hook'
__select__ = Hook.__select__ & implements('EmailAddress')
__select__ = Hook.__select__ & is_instance('EmailAddress')
events = ('before_update_entity',)
def __call__(self):
# safety belt: avoid potential infinite recursion if the test
......@@ -408,7 +408,7 @@ class RepositoryTC(CubicWebTC):
# local hook
class DummyBeforeHook(Hook):
__regid__ = 'dummy-before-hook'
__select__ = Hook.__select__ & implements('EmailAddress')
__select__ = Hook.__select__ & is_instance('EmailAddress')
events = ('before_add_entity',)
def __call__(self):
# set_attributes is forbidden within before_add_entity()
......@@ -427,7 +427,7 @@ class RepositoryTC(CubicWebTC):
class DummyBeforeHook(Hook):
_test = self # keep reference to test instance
__regid__ = 'dummy-before-hook'
__select__ = Hook.__select__ & implements('Affaire')
__select__ = Hook.__select__ & is_instance('Affaire')
events = ('before_update_entity',)
def __call__(self):
# invoiced attribute shouldn't be considered "edited" before the hook
......
......@@ -27,13 +27,13 @@ import shutil
import tempfile
from cubicweb import Binary, QueryError
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.server.sources import storages
from cubicweb.server.hook import Hook, Operation
class DummyBeforeHook(Hook):
__regid__ = 'dummy-before-hook'
__select__ = Hook.__select__ & implements('File')
__select__ = Hook.__select__ & is_instance('File')
events = ('before_add_entity',)
def __call__(self):
......@@ -42,7 +42,7 @@ class DummyBeforeHook(Hook):
class DummyAfterHook(Hook):
__regid__ = 'dummy-after-hook'
__select__ = Hook.__select__ & implements('File')
__select__ = Hook.__select__ & is_instance('File')
events = ('after_add_entity',)
def __call__(self):
......
......@@ -15,11 +15,9 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""
"""
from cubicweb.selectors import implements
from cubicweb.selectors import is_instance
from cubicweb.sobjects.notification import StatusChangeMixIn, NotificationView
class UserStatusChangeView(StatusChangeMixIn, NotificationView):
__select__ = NotificationView.__select__ & implements('CWUser')
__select__ = NotificationView.__select__ & is_instance('CWUser')
......@@ -22,7 +22,7 @@ from logilab.common.testlib import TestCase, unittest_main
from cubicweb import Binary
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.appobject import Selector, AndSelector, OrSelector
from cubicweb.selectors import implements, adaptable, match_user_groups
from cubicweb.selectors import is_instance, adaptable, match_user_groups
from cubicweb.interfaces import IDownloadable
from cubicweb.web import action
......@@ -92,12 +92,12 @@ class SelectorsTC(TestCase):
self.assertEquals(selector(None), 2)
def test_search_selectors(self):
sel = implements('something')
self.assertIs(sel.search_selector(implements), sel)
sel = is_instance('something')
self.assertIs(sel.search_selector(is_instance), sel)
csel = AndSelector(sel, Selector())
self.assertIs(csel.search_selector(implements), sel)
self.assertIs(csel.search_selector(is_instance), sel)
csel = AndSelector(Selector(), sel)
self.assertIs(csel.search_selector(implements), sel)
self.assertIs(csel.search_selector(is_instance), sel)
def test_inplace_and(self):
selector = _1_()
......@@ -141,15 +141,15 @@ class ImplementsSelectorTC(CubicWebTC):
req = self.request()
f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
rset = f.as_rset()
anyscore = implements('Any')(f.__class__, req, rset=rset)
anyscore = is_instance('Any')(f.__class__, req, rset=rset)
idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
self.failUnless(idownscore > anyscore, (idownscore, anyscore))
filescore = implements('File')(f.__class__, req, rset=rset)
filescore = is_instance('File')(f.__class__, req, rset=rset)