Commit 8fec1e43 authored by Aurelien Campeas's avatar Aurelien Campeas
Browse files

[repoapi] fold ClientConnection into Connection

Connection replaces ClientConnection everywhere.

Some notes:

* testlib: .client_cnx and .repo_cnx become aliases of .cnx (we might
  not want to tell people to update their tests again for just no real
  benefit, so we'll live with these aliases for a while)

* entity.as_rset must not be cached because we risk caching result
  sets having a Connection object as .req (helps unittest_breadcrumbs)

* entity._cw_dont_cache_attributes loses its repo/request special
  paths and only keeps its storage/bfss user (this helps
  unittest_wfobjs)

* moreover, entity.cw_instantiate and .cw_set stop overriding the
  attributes cache *after* the before_*_entity hooks have run, because
  there is no need to (it is now actually harmful to do it and
  unittest_hooks.test_html_tidy* tests remain green because of this)

* rset._build_entity sticks its .req onto the entity just fetched from
  the cache, because otherwise it might carry a _cw that is a
  Connection object where a Request is expected (helps
  unittest_views_actions)

* we get overall better cache usages (entity caches were split over
  Request + ClientConnection and Connection), hence the changes
  unittest_entity and unittest_wfobjs

* void the ecache when providing the cnx to a request object

  Having the entity cache pre-filled when we bind it to the request
  object hurts because these entities are bound to Connection objects,
  that lack e.g. `.form` or `.add_js` and crash the views subsystem.

  Thus, the unittest_testlib.test_error_raised test will are kept
  green.


Closes #3837233
parent d9a1e7939ee6
......@@ -494,7 +494,7 @@ class TestDataBaseHandler(object):
repo = self.get_repo(startup=True)
cnx = self.get_cnx()
with cnx:
pre_setup_func(cnx._cnx, self.config)
pre_setup_func(cnx, self.config)
cnx.commit()
self.backup_database(test_db_id)
......
......@@ -189,8 +189,7 @@ class RepoAccess(object):
A repo access can create three type of object:
.. automethod:: cubicweb.testlib.RepoAccess.repo_cnx
.. automethod:: cubicweb.testlib.RepoAccess.client_cnx
.. automethod:: cubicweb.testlib.RepoAccess.cnx
.. automethod:: cubicweb.testlib.RepoAccess.web_request
The RepoAccess need to be closed to destroy the associated Session.
......@@ -225,16 +224,13 @@ class RepoAccess(object):
return session
@contextmanager
def repo_cnx(self):
def cnx(self):
"""Context manager returning a server side connection for the user"""
with self._session.new_cnx() as cnx:
yield cnx
@contextmanager
def client_cnx(self):
"""Context manager returning a client side connection for the user"""
with repoapi.ClientConnection(self._session) as cnx:
yield cnx
# aliases for bw compat
client_cnx = repo_cnx = cnx
@contextmanager
def web_request(self, url=None, headers={}, method='GET', **kwargs):
......@@ -247,9 +243,10 @@ class RepoAccess(object):
"""
req = self.requestcls(self._repo.vreg, url=url, headers=headers,
method=method, form=kwargs)
clt_cnx = repoapi.ClientConnection(self._session)
req.set_cnx(clt_cnx)
with clt_cnx:
with self._session.new_cnx() as cnx:
if 'ecache' in cnx.transaction_data:
del cnx.transaction_data['ecache']
req.set_cnx(cnx)
yield req
def close(self):
......@@ -349,9 +346,7 @@ class CubicWebTC(TestCase):
def _close_cnx(self):
"""ensure that all cnx used by a test have been closed"""
for cnx in list(self._cnxs):
if cnx._open and not cnx._session.closed:
cnx.rollback()
cnx.close()
cnx.rollback()
self._cnxs.remove(cnx)
@property
......@@ -416,7 +411,7 @@ class CubicWebTC(TestCase):
login = unicode(db_handler.config.default_admin_config['login'])
self.admin_access = self.new_access(login)
self._admin_session = self.admin_access._session
self._admin_clt_cnx = repoapi.ClientConnection(self._admin_session)
self._admin_clt_cnx = repoapi.Connection(self._admin_session)
self._cnxs.add(self._admin_clt_cnx)
self._admin_clt_cnx.__enter__()
self.config.repository = lambda x=None: self.repo
......@@ -528,8 +523,6 @@ class CubicWebTC(TestCase):
def tearDown(self):
# XXX hack until logilab.common.testlib is fixed
if self._admin_clt_cnx is not None:
if self._admin_clt_cnx._open:
self._admin_clt_cnx.close()
self._admin_clt_cnx = None
if self._admin_session is not None:
self.repo.close(self._admin_session.sessionid)
......@@ -976,7 +969,7 @@ class CubicWebTC(TestCase):
def assertAuthSuccess(self, req, origsession, nbsessions=1):
sh = self.app.session_handler
session = self.app.get_session(req)
clt_cnx = repoapi.ClientConnection(session)
clt_cnx = repoapi.Connection(session)
req.set_cnx(clt_cnx)
self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
self.assertEqual(session.login, origsession.login)
......@@ -1263,7 +1256,7 @@ class AutoPopulateTest(CubicWebTC):
"""this method populates the database with `how_many` entities
of each possible type. It also inserts random relations between them
"""
with self.admin_access.repo_cnx() as cnx:
with self.admin_access.cnx() as cnx:
with cnx.security_enabled(read=False, write=False):
self._auto_populate(cnx, how_many)
cnx.commit()
......
......@@ -569,8 +569,9 @@ class AutoTransitionTC(CubicWebTC):
with self.admin_access.web_request() as req:
user = self.create_user(req, 'member', surname=u'toto')
req.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
{'wf': wf.eid, 'x': user.eid})
{'wf': wf.eid, 'x': user.eid})
req.cnx.commit()
user.cw_clear_all_caches()
iworkflowable = user.cw_adapt_to('IWorkflowable')
self.assertEqual(iworkflowable.state, 'dead')
......
......@@ -514,7 +514,7 @@ class Entity(AppObject):
prefixing the relation name by 'reverse_'. Also, relation values may be
an entity or eid, a list of entities or eids.
"""
rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
rql, qargs, pendingrels, _attrcache = cls._cw_build_entity_query(kwargs)
if rql:
rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
else:
......@@ -524,7 +524,6 @@ class Entity(AppObject):
except IndexError:
raise Exception('could not create a %r with %r (%r)' %
(cls.__regid__, rql, qargs))
created._cw_update_attr_cache(attrcache)
cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
return created
......@@ -556,41 +555,26 @@ class Entity(AppObject):
return super(Entity, self).__hash__()
def _cw_update_attr_cache(self, attrcache):
# if context is a repository session, don't consider dont-cache-attrs as
# the instance already holds modified values and loosing them could
# introduce severe problems
trdata = self._cw.transaction_data
uncached_attrs = trdata.get('%s.storage-special-process-attrs' % self.eid, set())
if self._cw.is_request:
uncached_attrs.update(trdata.get('%s.dont-cache-attrs' % self.eid, set()))
for attr in uncached_attrs:
attrcache.pop(attr, None)
self.cw_attr_cache.pop(attr, None)
self.cw_attr_cache.update(attrcache)
def _cw_dont_cache_attribute(self, attr, repo_side=False):
"""Repository side method called when some attribute has been
transformed by a hook, hence original value should not be cached by
the client.
If repo_side is True, this means that the attribute has been
transformed by a *storage*, hence the original value should
not be cached **by anyone**.
This only applies to a storage special case where the value
specified in creation or update is **not** the value that will
be transparently exposed later.
"""Called when some attribute has been transformed by a *storage*,
hence the original value should not be cached **by anyone**.
For example we have a special "fs_importing" mode in BFSS
where a file path is given as attribute value and stored as is
in the data base. Later access to the attribute will provide
the content of the file at the specified path. We do not want
the "filepath" value to be cached.
"""
self._cw.transaction_data.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
if repo_side:
trdata = self._cw.transaction_data
trdata.setdefault('%s.storage-special-process-attrs' % self.eid, set()).add(attr)
trdata = self._cw.transaction_data
trdata.setdefault('%s.storage-special-process-attrs' % self.eid, set()).add(attr)
def __json_encode__(self):
"""custom json dumps hook to dump the entity's eid
......@@ -836,7 +820,6 @@ class Entity(AppObject):
# data fetching methods ###################################################
@cached
def as_rset(self): # XXX .cw_as_rset
"""returns a resultset containing `self` information"""
rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
......@@ -1329,10 +1312,6 @@ class Entity(AppObject):
else:
rql += ' WHERE X eid %(x)s'
self._cw.execute(rql, qargs)
# update current local object _after_ the rql query to avoid
# interferences between the query execution itself and the cw_edited /
# skip_security machinery
self._cw_update_attr_cache(attrcache)
self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute)
# XXX update relation cache
......
......@@ -17,14 +17,12 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""Official API to access the content of a repository
"""
from logilab.common.deprecation import deprecated
from logilab.common.deprecation import class_deprecated
from cubicweb.utils import parse_repo_uri
from cubicweb import ConnectionError, ProgrammingError, AuthenticationError
from uuid import uuid4
from contextlib import contextmanager
from cubicweb.req import RequestSessionBase
from functools import wraps
from cubicweb import ConnectionError, AuthenticationError
from cubicweb.server.session import Connection
### private function for specific method ############################
......@@ -65,7 +63,7 @@ def connect(repo, login, **kwargs):
session = repo._get_session(sessionid)
# XXX the autoclose_session should probably be handle on the session directly
# this is something to consider once we have proper server side Connection.
return ClientConnection(session, autoclose_session=True)
return Connection(session)
def anonymous_cnx(repo):
"""return a ClientConnection for Anonymous user.
......@@ -82,292 +80,7 @@ def anonymous_cnx(repo):
# use vreg's repository cache
return connect(repo, anon_login, password=anon_password)
def _srv_cnx_func(name):
"""Decorate ClientConnection method blindly forward to Connection
THIS TRANSITIONAL PURPOSE
will be dropped when we have standalone connection"""
def proxy(clt_cnx, *args, **kwargs):
# the ``with`` dance is transitional. We do not have Standalone
# Connection yet so we use this trick to unsure the session have the
# proper cnx loaded. This can be simplified one we have Standalone
# Connection object
if not clt_cnx._open:
raise ProgrammingError('Closed client connection')
return getattr(clt_cnx._cnx, name)(*args, **kwargs)
return proxy
def _open_only(func):
"""decorator for ClientConnection method that check it is open"""
@wraps(func)
def check_open(clt_cnx, *args, **kwargs):
if not clt_cnx._open:
raise ProgrammingError('Closed client connection')
return func(clt_cnx, *args, **kwargs)
return check_open
class ClientConnection(RequestSessionBase):
"""A Connection object to be used Client side.
This object is aimed to be used client side (so potential communication
with the repo through RPC) and aims to offer some compatibility with the
cubicweb.dbapi.Connection interface.
The autoclose_session parameter informs the connection that this session
has been opened explicitly and only for this client connection. The
connection will close the session on exit.
"""
# make exceptions available through the connection object
ProgrammingError = ProgrammingError
# attributes that may be overriden per connection instance
anonymous_connection = False # XXX really needed ?
is_repo_in_memory = True # BC, always true
def __init__(self, session, autoclose_session=False):
super(ClientConnection, self).__init__(session.vreg)
self._session = session # XXX there is no real reason to keep the
# session around function still using it should
# be rewritten and migrated.
self._cnx = None
self._open = None
self._web_request = False
#: cache entities built during the connection
self._eid_cache = {}
self._set_user(session.user)
self._autoclose_session = autoclose_session
def __enter__(self):
assert self._open is None
self._open = True
self._cnx = self._session.new_cnx()
self._cnx.__enter__()
self._cnx.ctx_count += 1
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._open = False
self._cnx.ctx_count -= 1
self._cnx.__exit__(exc_type, exc_val, exc_tb)
self._cnx = None
if self._autoclose_session:
# we have to call repo.close to ensure the repo properly forgets the
# session; calling session.close() is not enough :-(
self._session.repo.close(self._session.sessionid)
# begin silly BC
@property
def _closed(self):
return not self._open
def close(self):
if self._open:
self.__exit__(None, None, None)
def __repr__(self):
# XXX we probably want to reference the user of the session here
if self._open is None:
return '<ClientConnection (not open yet)>'
elif not self._open:
return '<ClientConnection (closed)>'
elif self.anonymous_connection:
return '<ClientConnection %s (anonymous)>' % self._cnx.connectionid
else:
return '<ClientConnection %s>' % self._cnx.connectionid
# end silly BC
# Main Connection purpose in life #########################################
call_service = _srv_cnx_func('call_service')
@_open_only
def execute(self, *args, **kwargs):
# the ``with`` dance is transitional. We do not have Standalone
# Connection yet so we use this trick to unsure the session have the
# proper cnx loaded. This can be simplified one we have Standalone
# Connection object
rset = self._cnx.execute(*args, **kwargs)
rset.req = self
return rset
@_open_only
def commit(self, *args, **kwargs):
try:
return self._cnx.commit(*args, **kwargs)
finally:
self.drop_entity_cache()
@_open_only
def rollback(self, *args, **kwargs):
try:
return self._cnx.rollback(*args, **kwargs)
finally:
self.drop_entity_cache()
# security #################################################################
allow_all_hooks_but = _srv_cnx_func('allow_all_hooks_but')
deny_all_hooks_but = _srv_cnx_func('deny_all_hooks_but')
security_enabled = _srv_cnx_func('security_enabled')
# direct sql ###############################################################
system_sql = _srv_cnx_func('system_sql')
# session data methods #####################################################
get_shared_data = _srv_cnx_func('get_shared_data')
set_shared_data = _srv_cnx_func('set_shared_data')
@property
def transaction_data(self):
return self._cnx.transaction_data
# meta-data accessors ######################################################
@_open_only
def source_defs(self):
"""Return the definition of sources used by the repository."""
return self._session.repo.source_defs()
@_open_only
def get_schema(self):
"""Return the schema currently used by the repository."""
return self._session.repo.source_defs()
@_open_only
def get_option_value(self, option):
"""Return the value for `option` in the configuration."""
return self._session.repo.get_option_value(option)
entity_metas = _srv_cnx_func('entity_metas')
describe = _srv_cnx_func('describe') # XXX deprecated in 3.19
# undo support ############################################################
@_open_only
def undoable_transactions(self, ueid=None, req=None, **actionfilters):
"""Return a list of undoable transaction objects by the connection's
user, ordered by descendant transaction time.
Managers may filter according to user (eid) who has done the transaction
using the `ueid` argument. Others will only see their own transactions.
Additional filtering capabilities is provided by using the following
named arguments:
* `etype` to get only transactions creating/updating/deleting entities
of the given type
* `eid` to get only transactions applied to entity of the given eid
* `action` to get only transactions doing the given action (action in
'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
'D'.
* `public`: when additional filtering is provided, their are by default
only searched in 'public' actions, unless a `public` argument is given
and set to false.
"""
# the ``with`` dance is transitional. We do not have Standalone
# Connection yet so we use this trick to unsure the session have the
# proper cnx loaded. This can be simplified one we have Standalone
# Connection object
source = self._cnx.repo.system_source
txinfos = source.undoable_transactions(self._cnx, ueid, **actionfilters)
for txinfo in txinfos:
txinfo.req = req or self # XXX mostly wrong
return txinfos
@_open_only
def transaction_info(self, txuuid, req=None):
"""Return transaction object for the given uid.
raise `NoSuchTransaction` if not found or if session's user is not
allowed (eg not in managers group and the transaction doesn't belong to
him).
"""
# the ``with`` dance is transitional. We do not have Standalone
# Connection yet so we use this trick to unsure the session have the
# proper cnx loaded. This can be simplified one we have Standalone
# Connection object
txinfo = self._cnx.repo.system_source.tx_info(self._cnx, txuuid)
if req:
txinfo.req = req
else:
txinfo.cnx = self
return txinfo
@_open_only
def transaction_actions(self, txuuid, public=True):
"""Return an ordered list of action effectued during that transaction.
If public is true, return only 'public' actions, eg not ones triggered
under the cover by hooks, else return all actions.
raise `NoSuchTransaction` if the transaction is not found or if
session's user is not allowed (eg not in managers group and the
transaction doesn't belong to him).
"""
# the ``with`` dance is transitional. We do not have Standalone
# Connection yet so we use this trick to unsure the session have the
# proper cnx loaded. This can be simplified one we have Standalone
# Connection object
return self._cnx.repo.system_source.tx_actions(self._cnx, txuuid, public)
@_open_only
def undo_transaction(self, txuuid):
"""Undo the given transaction. Return potential restoration errors.
raise `NoSuchTransaction` if not found or if session's user is not
allowed (eg not in managers group and the transaction doesn't belong to
him).
"""
# the ``with`` dance is transitional. We do not have Standalone
# Connection yet so we use this trick to unsure the session have the
# proper cnx loaded. This can be simplified one we have Standalone
# Connection object
return self._cnx.repo.system_source.undo_transaction(self._cnx, txuuid)
# cache management
def entity_cache(self, eid):
return self._eid_cache[eid]
def set_entity_cache(self, entity):
self._eid_cache[entity.eid] = entity
def cached_entities(self):
return self._eid_cache.values()
def drop_entity_cache(self, eid=None):
if eid is None:
self._eid_cache = {}
else:
del self._eid_cache[eid]
# deprecated stuff
@deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
def request(self):
return self
@deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
def cursor(self):
return self
@property
@deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
def sessionid(self):
return self._session.sessionid
@property
@deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
def connection(self):
return self
@property
@deprecated('[3.19] This is a repoapi.ClientConnection object not a dbapi one')
def _repo(self):
return self._session.repo
class ClientConnection(Connection):
__metaclass__ = class_deprecated
__deprecation_warning__ = '[3.20] %(cls)s is deprecated, use Connection instead'
......@@ -483,6 +483,7 @@ class ResultSet(object):
# new attributes found in this resultset ?
try:
entity = req.entity_cache(eid)
entity._cw = req
except KeyError:
pass
else:
......
......@@ -289,7 +289,6 @@ def init_repository(config, interactive=True, drop=False, vreg=None,
config._cubes = None # avoid assertion error
repo = get_repository(config=config)
with connect(repo, login, password=pwd) as cnx:
cnx = cnx._cnx # client connection -> repo connection
with cnx.security_enabled(False, False):
repo.system_source.eid = ssource.eid # redo this manually
handler = config.migration_handler(schema, interactive=False,
......
......@@ -103,8 +103,6 @@ class EditedEntity(dict):
assert not self.saved, 'too late to modify edited attributes'
super(EditedEntity, self).__setitem__(attr, value)
self.entity.cw_attr_cache[attr] = value
# mark attribute as needing purge by the client
self.entity._cw_dont_cache_attribute(attr)
def oldnewvalue(self, attr):
"""returns the couple (old attr value, new attr value)
......
......@@ -812,11 +812,6 @@ class Repository(object):
return self._extid_cache[extid]
except KeyError:
pass
try:
# bw compat: cnx may be a session, get at the Connection
cnx = cnx._cnx
except AttributeError:
pass
with cnx.ensure_cnx_set:
eid = self.system_source.extid2eid(cnx, extid)
if eid is not None:
......
......@@ -957,7 +957,7 @@ class RebuildFTICommand(Command):
config = ServerConfiguration.config_for(appid)
repo, cnx = repo_cnx(config)
with cnx:
reindex_entities(repo.schema, cnx._cnx, etypes=etypes)
reindex_entities(repo.schema, cnx, etypes=etypes)
cnx.commit()
......
......@@ -152,7 +152,7 @@ class BytesFileSystemStorage(Storage):
"""an entity using this storage for attr has been added"""
if entity._cw.transaction_data.get('fs_importing'):
binary = Binary.from_file(entity.cw_edited[attr].getvalue())
entity._cw_dont_cache_attribute(attr, repo_side=True)
entity._cw_dont_cache_attribute(attr)
else:
binary = entity.cw_edited.pop(attr)
fpath = self.new_fs_path(entity, attr)
......@@ -171,7 +171,7 @@ class BytesFileSystemStorage(Storage):
# We do not need to create it but we need to fetch the content of
# the file as the actual content of the attribute
fpath = entity.cw_edited[attr].getvalue()
entity._cw_dont_cache_attribute(attr, repo_side=True)
entity._cw_dont_cache_attribute(attr)
assert fpath is not None
binary = Binary.from_file(fpath)
else:
......
......@@ -69,7 +69,6 @@ def setUpClass(cls, *args):
def tearDownClass(cls, *args):
global repo, cnx
cnx.close()
repo.shutdown()
del repo, cnx
......
......@@ -140,13 +140,24 @@ class EntityTC(CubicWebTC):
with self.admin_access.web_request() as req:
user = req.execute('Any X WHERE X eid %(x)s', {'x':req.user.eid}).get_entity(0, 0)
adeleid = req.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
self.assertEqual({}, user._cw_related_cache)
req.cnx.commit()
self.assertEqual(user._cw_related_cache, {})
self.assertEqual(['primary_email_subject', 'use_email_subject', 'wf_info_for_object'],
sorted(user._cw_related_cache))
email = user.primary_email[0]
self.assertEqual(sorted(user._cw_related_cache), ['primary_email_subject'])
self.assertEqual(list(email._cw_related_cache), ['primary_email_object'])
self.assertEqual(u'toto@logilab.org', email.address)
self.assertEqual(['created_by_subject',
'cw_source_subject',
'is_instance_of_subject',
'is_subject',
'owned_by_subject',
'prefered_form_object',
'prefered_form_subject',
'primary_email_object',
'use_email_object'],
sorted(email._cw_related_cache))
self.assertEqual('admin', email._cw_related_cache['primary_email_object'][1][0].login)
groups = user.in_group
self.assertEqual(sorted(user._cw_related_cache), ['in_group_subject', 'primary_email_subject'])
for group in groups: