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

[appobject] kill VObject class, move base selector classes to appobject

parent a93ae0f6c0ad
"""Base class for dynamically loaded objects manipulated in the web interface
"""Base class for dynamically loaded objects accessible through the vregistry.
You'll also find some convenience classes to build selectors.
:organization: Logilab
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
......@@ -7,20 +9,23 @@
"""
__docformat__ = "restructuredtext en"
import types
from logging import getLogger
from datetime import datetime, timedelta, time
from logilab.common.decorators import classproperty
from logilab.common.deprecation import deprecated
from logilab.common.logging_ext import set_log_methods
from rql.nodes import VariableRef, SubQuery
from rql.stmts import Union, Select
from cubicweb import Unauthorized, NoSelectableObject
from cubicweb.vregistry import VObject, AndSelector
from cubicweb.selectors import yes
from cubicweb.utils import UStringIO, ustrftime, strptime, todate, todatetime
ONESECOND = timedelta(0, 1, 0)
CACHE_REGISTRY = {}
class Cache(dict):
def __init__(self):
......@@ -29,14 +34,200 @@ class Cache(dict):
self.cache_creation_date = _now
self.latest_cache_lookup = _now
CACHE_REGISTRY = {}
class AppObject(VObject):
"""This is the base class for CubicWeb application objects
which are selected according to a request and result set.
# selector base classes and operations ########################################
def objectify_selector(selector_func):
"""convenience decorator for simple selectors where a class definition
would be overkill::
@objectify_selector
def yes(cls, *args, **kwargs):
return 1
"""
return type(selector_func.__name__, (Selector,),
{'__call__': lambda self, *args, **kwargs: selector_func(*args, **kwargs)})
def _instantiate_selector(selector):
"""ensures `selector` is a `Selector` instance
NOTE: This should only be used locally in build___select__()
XXX: then, why not do it ??
"""
if isinstance(selector, types.FunctionType):
return objectify_selector(selector)()
if isinstance(selector, type) and issubclass(selector, Selector):
return selector()
return selector
class Selector(object):
"""base class for selector classes providing implementation
for operators ``&`` and ``|``
This class is only here to give access to binary operators, the
selector logic itself should be implemented in the __call__ method
a selector is called to help choosing the correct object for a
particular context by returning a score (`int`) telling how well
the class given as first argument apply to the given context.
0 score means that the class doesn't apply.
"""
@property
def func_name(self):
# backward compatibility
return self.__class__.__name__
def search_selector(self, selector):
"""search for the given selector or selector instance in the selectors
tree. Return it of None if not found
"""
if self is selector:
return self
if isinstance(selector, type) and isinstance(self, selector):
return self
return None
def __str__(self):
return self.__class__.__name__
def __and__(self, other):
return AndSelector(self, other)
def __rand__(self, other):
return AndSelector(other, self)
def __or__(self, other):
return OrSelector(self, other)
def __ror__(self, other):
return OrSelector(other, self)
def __invert__(self):
return NotSelector(self)
# XXX (function | function) or (function & function) not managed yet
def __call__(self, cls, *args, **kwargs):
return NotImplementedError("selector %s must implement its logic "
"in its __call__ method" % self.__class__)
Classes are kept in the vregistry and instantiation is done at selection
time.
class MultiSelector(Selector):
"""base class for compound selector classes"""
def __init__(self, *selectors):
self.selectors = self.merge_selectors(selectors)
def __str__(self):
return '%s(%s)' % (self.__class__.__name__,
','.join(str(s) for s in self.selectors))
@classmethod
def merge_selectors(cls, selectors):
"""deal with selector instanciation when necessary and merge
multi-selectors if possible:
AndSelector(AndSelector(sel1, sel2), AndSelector(sel3, sel4))
==> AndSelector(sel1, sel2, sel3, sel4)
"""
merged_selectors = []
for selector in selectors:
try:
selector = _instantiate_selector(selector)
except:
pass
#assert isinstance(selector, Selector), selector
if isinstance(selector, cls):
merged_selectors += selector.selectors
else:
merged_selectors.append(selector)
return merged_selectors
def search_selector(self, selector):
"""search for the given selector or selector instance in the selectors
tree. Return it of None if not found
"""
for childselector in self.selectors:
if childselector is selector:
return childselector
found = childselector.search_selector(selector)
if found is not None:
return found
return None
class AndSelector(MultiSelector):
"""and-chained selectors (formerly known as chainall)"""
def __call__(self, cls, *args, **kwargs):
score = 0
for selector in self.selectors:
partscore = selector(cls, *args, **kwargs)
if not partscore:
return 0
score += partscore
return score
class OrSelector(MultiSelector):
"""or-chained selectors (formerly known as chainfirst)"""
def __call__(self, cls, *args, **kwargs):
for selector in self.selectors:
partscore = selector(cls, *args, **kwargs)
if partscore:
return partscore
return 0
class NotSelector(Selector):
"""negation selector"""
def __init__(self, selector):
self.selector = selector
def __call__(self, cls, *args, **kwargs):
score = self.selector(cls, *args, **kwargs)
return int(not score)
def __str__(self):
return 'NOT(%s)' % super(NotSelector, self).__str__()
class yes(Selector):
"""return arbitrary score
default score of 0.5 so any other selector take precedence
"""
def __init__(self, score=0.5):
self.score = score
def __call__(self, *args, **kwargs):
return self.score
# the base class for all appobjects ############################################
class AppObject(object):
"""This is the base class for CubicWeb application objects which are
selected according to a context (usually at least a request and a result
set).
Concrete application objects classes are designed to be loaded by the
vregistry and should be accessed through it, not by direct instantiation.
The following attributes should be set on concret appobject classes:
:__registry__:
name of the registry for this object (string like 'views',
'templates'...)
:id:
object's identifier in the registry (string like 'main',
'primary', 'folder_box')
:__select__:
class'selector
Moreover, the `__abstract__` attribute may be set to True to indicate
that a appobject is abstract and should not be registered.
At registration time, the following attributes are set on the class:
:vreg:
......@@ -46,20 +237,64 @@ class AppObject(VObject):
:config:
the instance's configuration
At instantiation time, the following attributes are set on the instance:
At selection time, the following attributes are set on the instance:
:req:
current request
:rset:
result set on which the object is applied
context result set or None
:row:
if a result set is set and the context is about a particular cell in the
result set, and not the result set as a whole, specify the row number we
are interested in, else None
:col:
if a result set is set and the context is about a particular cell in the
result set, and not the result set as a whole, specify the col number we
are interested in, else None
"""
__registry__ = None
id = None
__select__ = yes()
@classmethod
def registered(cls, reg):
super(AppObject, cls).registered(reg)
cls.vreg = reg.vreg
cls.schema = reg.schema
cls.config = reg.config
def classid(cls):
"""returns a unique identifier for the appobject"""
return '%s.%s' % (cls.__module__, cls.__name__)
# XXX bw compat code
@classmethod
def build___select__(cls):
for klass in cls.mro():
if klass.__name__ == 'AppObject':
continue # the bw compat __selector__ is there
klassdict = klass.__dict__
if ('__select__' in klassdict and '__selectors__' in klassdict
and '__selgenerated__' not in klassdict):
raise TypeError("__select__ and __selectors__ can't be used together on class %s" % cls)
if '__selectors__' in klassdict and '__selgenerated__' not in klassdict:
cls.__selgenerated__ = True
# case where __selectors__ is defined locally (but __select__
# is in a parent class)
selectors = klassdict['__selectors__']
if len(selectors) == 1:
# micro optimization: don't bother with AndSelector if there's
# only one selector
select = _instantiate_selector(selectors[0])
else:
select = AndSelector(*selectors)
cls.__select__ = select
@classmethod
def registered(cls, registry):
"""called by the registry when the appobject has been registered.
It must return the object that will be actually registered (this may be
the right hook to create an instance for example). By default the
appobject is returned without any transformation.
"""
cls.build___select__()
cls.vreg = registry.vreg
cls.schema = registry.schema
cls.config = registry.config
cls.register_properties()
return cls
......@@ -69,9 +304,13 @@ class AppObject(VObject):
@classmethod
def selected(cls, *args, **kwargs):
"""by default web app objects are usually instantiated on
selection according to a request, a result set, and optional
row and col
"""called by the registry when the appobject has been selected.
It must return the object that will be actually returned by the .select
method (this may be the right hook to create an instance for
example). By default the selected object is called using the given args
and kwargs and the resulting value (usually a class instance) is
returned without any transformation.
"""
return cls(*args, **kwargs)
......@@ -340,3 +579,5 @@ class AppObject(VObject):
first = rql.split(' ', 1)[0].lower()
if first in ('insert', 'set', 'delete'):
raise Unauthorized(self.req._('only select queries are authorized'))
set_log_methods(AppObject, getLogger('cubicweb.appobject'))
......@@ -37,8 +37,8 @@ class MigrationToolsTC(TestCase):
self.config = MigrTestConfig('data')
from yams.schema import Schema
self.config.load_schema = lambda expand_cubes=False: Schema('test')
self.config.__class__.cubicweb_vobject_path = frozenset()
self.config.__class__.cube_vobject_path = frozenset()
self.config.__class__.cubicweb_appobject_path = frozenset()
self.config.__class__.cube_appobject_path = frozenset()
def test_filter_scripts_base(self):
self.assertListEquals(filter_scripts(self.config, SMIGRDIR, (2,3,0), (2,4,0)),
......
......@@ -142,7 +142,7 @@ class CubicWebNoAppConfiguration(ConfigurationMixIn):
name = None
# log messages format (see logging module documentation for available keys)
log_format = '%(asctime)s - (%(name)s) %(levelname)s: %(message)s'
# nor remove vobjects based on unused interface
# nor remove appobjects based on unused interface
cleanup_interface_sobjects = True
if os.environ.get('APYCOT_ROOT'):
......@@ -419,8 +419,8 @@ this option is set to yes",
except Exception, ex:
cls.warning("can't init cube %s: %s", cube, ex)
cubicweb_vobject_path = set(['entities'])
cube_vobject_path = set(['entities'])
cubicweb_appobject_path = set(['entities'])
cube_appobject_path = set(['entities'])
@classmethod
def build_vregistry_path(cls, templpath, evobjpath=None, tvobjpath=None):
......@@ -430,13 +430,13 @@ this option is set to yes",
:param evobjpath:
optional list of sub-directories (or files without the .py ext) of
the cubicweb library that should be tested and added to the output list
if they exists. If not give, default to `cubicweb_vobject_path` class
if they exists. If not give, default to `cubicweb_appobject_path` class
attribute.
:param tvobjpath:
optional list of sub-directories (or files without the .py ext) of
directories given in `templpath` that should be tested and added to
the output list if they exists. If not give, default to
`cube_vobject_path` class attribute.
`cube_appobject_path` class attribute.
"""
vregpath = cls.build_vregistry_cubicweb_path(evobjpath)
vregpath += cls.build_vregistry_cube_path(templpath, tvobjpath)
......@@ -446,7 +446,7 @@ this option is set to yes",
def build_vregistry_cubicweb_path(cls, evobjpath=None):
vregpath = []
if evobjpath is None:
evobjpath = cls.cubicweb_vobject_path
evobjpath = cls.cubicweb_appobject_path
for subdir in evobjpath:
path = join(CW_SOFTWARE_ROOT, subdir)
if exists(path):
......@@ -457,7 +457,7 @@ this option is set to yes",
def build_vregistry_cube_path(cls, templpath, tvobjpath=None):
vregpath = []
if tvobjpath is None:
tvobjpath = cls.cube_vobject_path
tvobjpath = cls.cube_appobject_path
for directory in templpath:
for subdir in tvobjpath:
path = join(directory, subdir)
......
......@@ -34,7 +34,7 @@ def use_interfaces(obj):
if impl:
return sorted(impl.expected_ifaces)
except AttributeError:
pass # old-style vobject classes with no accepts_interfaces
pass # old-style appobject classes with no accepts_interfaces
except:
print 'bad selector %s on %s' % (obj.__select__, obj)
raise
......@@ -308,7 +308,7 @@ class CubicWebVRegistry(VRegistry):
# we may want to keep interface dependent objects (e.g.for i18n
# catalog generation)
if self.config.cleanup_interface_sobjects:
# remove vobjects that don't support any available interface
# remove appobjects that don't support any available interface
implemented_interfaces = set()
if 'Any' in self.get('etypes', ()):
for etype in self.schema.entities():
......@@ -323,7 +323,7 @@ class CubicWebVRegistry(VRegistry):
or iface
for iface in ifaces)
if not ('Any' in ifaces or ifaces & implemented_interfaces):
self.debug('kicking vobject %s (no implemented '
self.debug('kicking appobject %s (no implemented '
'interface among %s)', obj, ifaces)
self.unregister(obj)
# clear needs_iface so we don't try to remove some not-anymore-in
......
......@@ -17,6 +17,7 @@ from itertools import count
from logilab.common.logging_ext import set_log_methods
from logilab.common.decorators import monkeypatch
from logilab.common.deprecation import deprecated
from cubicweb import ETYPE_NAME_MAP, ConnectionError, RequestSessionMixIn
from cubicweb import cwvreg, cwconfig
......@@ -29,10 +30,10 @@ def _fake_property_value(self, name):
except KeyError:
return ''
def _fix_cls_attrs(reg, vobject):
vobject.vreg = reg.vreg
vobject.schema = reg.schema
vobject.config = reg.config
def _fix_cls_attrs(reg, appobject):
appobject.vreg = reg.vreg
appobject.schema = reg.schema
appobject.config = reg.config
def multiple_connections_fix():
"""some monkey patching necessary when an application has to deal with
......@@ -43,15 +44,15 @@ def multiple_connections_fix():
defaultcls = cwvreg.VRegistry.REGISTRY_FACTORY[None]
orig_select_best = defaultcls.orig_select_best = defaultcls.select_best
@monkeypatch(defaultcls)
def select_best(self, vobjects, *args, **kwargs):
def select_best(self, appobjects, *args, **kwargs):
"""return an instance of the most specific object according
to parameters
raise NoSelectableObject if no object apply
"""
for vobjectcls in vobjects:
_fix_cls_attrs(self, vobjectcls)
selected = orig_select_best(self, vobjects, *args, **kwargs)
for appobjectcls in appobjects:
_fix_cls_attrs(self, appobjectcls)
selected = orig_select_best(self, appobjects, *args, **kwargs)
# redo the same thing on the instance so it won't use equivalent class
# attributes (which may change)
_fix_cls_attrs(self, selected)
......@@ -448,7 +449,7 @@ class Connection(object):
raise ProgrammingError('Closed connection')
return self._repo.get_schema()
def load_vobjects(self, cubes=_MARKER, subpath=None, expand=True,
def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True,
force_reload=None):
config = self.vreg.config
if cubes is _MARKER:
......@@ -481,11 +482,13 @@ class Connection(object):
if self._repo.config.instance_hooks:
hm.register_hooks(config.load_hooks(self.vreg))
load_vobjects = deprecated()(load_appobjects)
def use_web_compatible_requests(self, baseurl, sitetitle=None):
"""monkey patch DBAPIRequest to fake a cw.web.request, so you should
able to call html views using rset from a simple dbapi connection.
You should call `load_vobjects` at some point to register those views.
You should call `load_appobjects` at some point to register those views.
"""
from cubicweb.web.request import CubicWebRequestBase as cwrb
DBAPIRequest.build_ajax_replace_url = cwrb.build_ajax_replace_url.im_func
......
......@@ -142,8 +142,8 @@ class TestServerConfiguration(ServerConfiguration):
class BaseApptestConfiguration(TestServerConfiguration, TwistedConfiguration):
repo_method = 'inmemory'
options = merge_options(TestServerConfiguration.options + TwistedConfiguration.options)
cubicweb_vobject_path = TestServerConfiguration.cubicweb_vobject_path | TwistedConfiguration.cubicweb_vobject_path
cube_vobject_path = TestServerConfiguration.cube_vobject_path | TwistedConfiguration.cube_vobject_path
cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | TwistedConfiguration.cubicweb_appobject_path
cube_appobject_path = TestServerConfiguration.cube_appobject_path | TwistedConfiguration.cube_appobject_path
def available_languages(self, *args):
return ('en', 'fr', 'de')
......
......@@ -29,8 +29,8 @@ from cubicweb.server.serverconfig import ServerConfiguration
class DevCubeConfiguration(ServerConfiguration, WebConfiguration):
"""dummy config to get full library schema and entities"""
creating = True
cubicweb_vobject_path = ServerConfiguration.cubicweb_vobject_path | WebConfiguration.cubicweb_vobject_path
cube_vobject_path = ServerConfiguration.cube_vobject_path | WebConfiguration.cube_vobject_path
cubicweb_appobject_path = ServerConfiguration.cubicweb_appobject_path | WebConfiguration.cubicweb_appobject_path
cube_appobject_path = ServerConfiguration.cube_appobject_path | WebConfiguration.cube_appobject_path
def __init__(self, cube):
super(DevCubeConfiguration, self).__init__(cube)
......
......@@ -377,9 +377,9 @@ class RealDBTest(WebTest):
rset2 = rset.limit(limit=1, offset=row)
yield rset2
def not_selected(vreg, vobject):
def not_selected(vreg, appobject):
try:
vreg._selected[vobject.__class__] -= 1
vreg._selected[appobject.__class__] -= 1
except (KeyError, AttributeError):
pass
......@@ -405,7 +405,7 @@ def print_untested_objects(testclass, skipregs=('hooks', 'etypes')):
for regname, reg in testclass._env.vreg.iteritems():
if regname in skipregs:
continue
for vobjects in reg.itervalues():
for vobject in vobjects:
if not reg._selected.get(vobject):
print 'not tested', regname, vobject
for appobjects in reg.itervalues():
for appobject in appobjects:
if not reg._selected.get(appobject):
print 'not tested', regname, appobject
......@@ -266,7 +266,7 @@ class InterfaceTC(EnvBasedTC):
class MyUser(CWUser):
__implements__ = (IMileStone,)
self.vreg._loadedmods[__name__] = {}
self.vreg.register_vobject_class(MyUser)
self.vreg.register_appobject_class(MyUser)
self.failUnless(implements(CWUser, IWorkflowable))
self.failUnless(implements(MyUser, IMileStone))
self.failUnless(implements(MyUser, IWorkflowable))
......@@ -290,7 +290,7 @@ class SpecializedEntityClassesTC(EnvBasedTC):
for etype in ('Company', 'Division', 'SubDivision'):
class Foo(AnyEntity):
id = etype
self.vreg.register_vobject_class(Foo)
self.vreg.register_appobject_class(Foo)
eclass = self.select_eclass('SubDivision')
if etype == 'SubDivision':
self.failUnless(eclass is Foo)
......
......@@ -330,7 +330,7 @@ set_log_methods(CubicWebRootResource, getLogger('cubicweb.twisted'))
def _gc_debug():
import gc
from pprint import pprint
from cubicweb.vregistry import VObject
from cubicweb.appobject import AppObject
gc.collect()
count = 0
acount = 0
......@@ -338,7 +338,7 @@ def _gc_debug():
for obj in gc.get_objects():
if isinstance(obj, CubicWebTwistedRequestAdapter):
count += 1
elif isinstance(obj, VObject):
elif isinstance(obj, AppObject):
acount += 1
else:
try:
......
......@@ -87,8 +87,8 @@ try:
options = merge_options(TwistedConfiguration.options
+ ServerConfiguration.options)
cubicweb_vobject_path = TwistedConfiguration.cubicweb_vobject_path | ServerConfiguration.cubicweb_vobject_path
cube_vobject_path = TwistedConfiguration.cube_vobject_path | ServerConfiguration.cube_vobject_path
cubicweb_appobject_path = TwistedConfiguration.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
cube_appobject_path = TwistedConfiguration.cube_appobject_path | ServerConfiguration.cube_appobject_path
def pyro_enabled(self):
"""tell if pyro is activated for the in memory repository"""
return self['pyro-server']
......
......@@ -81,9 +81,9 @@ class GAEConfiguration(ServerConfiguration, WebConfiguration):
options = [(optname, optdict) for optname, optdict in options
if not optname in UNSUPPORTED_OPTIONS]
cubicweb_vobject_path = WebConfiguration.cubicweb_vobject_path | ServerConfiguration.cubicweb_vobject_path
cubicweb_vobject_path = list(cubicweb_vobject_path) + ['goa/appobjects']
cube_vobject_path = WebConfiguration.cube_vobject_path | ServerConfiguration.cube_vobject_path
cubicweb_appobject_path = WebConfiguration.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
cubicweb_appobject_path = list(cubicweb_appobject_path) + ['goa/appobjects']
cube_appobject_path = WebConfiguration.cube_appobject_path | ServerConfiguration.cube_appobject_path
# use file system schema
bootstrap_schema = read_instance_schema = False
......
......@@ -59,7 +59,7 @@ class GAEVRegistry(CubicWebVRegistry):
self.load_module(obj)
def _auto_load(self, path, loadschema, cube=None):
vobjpath = self.config.cube_vobject_path
vobjpath = self.config.cube_appobject_path
for filename in listdir(path):
if filename[-3:] == '.py' and filename[:-3] in vobjpath:
self._import(_pkg_name(cube, filename[:-3]))
......
......@@ -53,8 +53,8 @@ from yams import BASE_TYPES
from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
role, typed_eid)
from cubicweb.vregistry import (NoSelectableObject, Selector,
chainall, objectify_selector)
# even if not used, let yes here so it's importable through this module
from cubicweb.appobject import Selector, objectify_selector, yes
from cubicweb.cwconfig import CubicWebConfiguration
from cubicweb.schema import split_expression
......@@ -274,17 +274,6 @@ class EntitySelector(EClassSelector):