entity.py
brancholdstable
changeset 7074 e4580e5f0703
parent 6954 f9a84d54ebf3
child 7139 20807d3d7cf6
equal deleted inserted replaced
6749:48f468f33704 7074:e4580e5f0703
     1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """Base class for entity objects manipulated in clients"""
    18 """Base class for entity objects manipulated in clients"""
    19 
    19 
    20 __docformat__ = "restructuredtext en"
    20 __docformat__ = "restructuredtext en"
    21 
    21 
    22 from copy import copy
       
    23 from warnings import warn
    22 from warnings import warn
    24 
    23 
    25 from logilab.common import interface
    24 from logilab.common import interface
    26 from logilab.common.decorators import cached
    25 from logilab.common.decorators import cached
    27 from logilab.common.deprecation import deprecated
    26 from logilab.common.deprecation import deprecated
   311 
   310 
   312     def __repr__(self):
   311     def __repr__(self):
   313         return '<Entity %s %s %s at %s>' % (
   312         return '<Entity %s %s %s at %s>' % (
   314             self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
   313             self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
   315 
   314 
       
   315     def __cmp__(self, other):
       
   316         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
       
   317 
   316     def __json_encode__(self):
   318     def __json_encode__(self):
   317         """custom json dumps hook to dump the entity's eid
   319         """custom json dumps hook to dump the entity's eid
   318         which is not part of dict structure itself
   320         which is not part of dict structure itself
   319         """
   321         """
   320         dumpable = dict(self)
   322         dumpable = dict(self)
   321         dumpable['eid'] = self.eid
   323         dumpable['eid'] = self.eid
   322         return dumpable
   324         return dumpable
   323 
       
   324     def __nonzero__(self):
       
   325         return True
       
   326 
       
   327     def __hash__(self):
       
   328         return id(self)
       
   329 
       
   330     def __cmp__(self, other):
       
   331         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
       
   332 
       
   333     def __contains__(self, key):
       
   334         return key in self.cw_attr_cache
       
   335 
       
   336     def __iter__(self):
       
   337         return iter(self.cw_attr_cache)
       
   338 
       
   339     def __getitem__(self, key):
       
   340         if key == 'eid':
       
   341             warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
       
   342                  DeprecationWarning, stacklevel=2)
       
   343             return self.eid
       
   344         return self.cw_attr_cache[key]
       
   345 
       
   346     def __setitem__(self, attr, value):
       
   347         """override __setitem__ to update self.edited_attributes.
       
   348 
       
   349         Typically, a before_[update|add]_hook could do::
       
   350 
       
   351             entity['generated_attr'] = generated_value
       
   352 
       
   353         and this way, edited_attributes will be updated accordingly. Also, add
       
   354         the attribute to skip_security since we don't want to check security
       
   355         for such attributes set by hooks.
       
   356         """
       
   357         if attr == 'eid':
       
   358             warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
       
   359                  DeprecationWarning, stacklevel=2)
       
   360             self.eid = value
       
   361         else:
       
   362             self.cw_attr_cache[attr] = value
       
   363             # don't add attribute into skip_security if already in edited
       
   364             # attributes, else we may accidentaly skip a desired security check
       
   365             if hasattr(self, 'edited_attributes') and \
       
   366                    attr not in self.edited_attributes:
       
   367                 self.edited_attributes.add(attr)
       
   368                 self._cw_skip_security_attributes.add(attr)
       
   369 
       
   370     def __delitem__(self, attr):
       
   371         """override __delitem__ to update self.edited_attributes on cleanup of
       
   372         undesired changes introduced in the entity's dict. For example, see the
       
   373         code snippet below from the `forge` cube:
       
   374 
       
   375         .. sourcecode:: python
       
   376 
       
   377             edited = self.entity.edited_attributes
       
   378             has_load_left = 'load_left' in edited
       
   379             if 'load' in edited and self.entity.load_left is None:
       
   380                 self.entity.load_left = self.entity['load']
       
   381             elif not has_load_left and edited:
       
   382                 # cleanup, this may cause undesired changes
       
   383                 del self.entity['load_left']
       
   384 
       
   385         """
       
   386         del self.cw_attr_cache[attr]
       
   387         if hasattr(self, 'edited_attributes'):
       
   388             self.edited_attributes.remove(attr)
       
   389 
       
   390     def clear(self):
       
   391         self.cw_attr_cache.clear()
       
   392 
       
   393     def get(self, key, default=None):
       
   394         return self.cw_attr_cache.get(key, default)
       
   395 
       
   396     def setdefault(self, attr, default):
       
   397         """override setdefault to update self.edited_attributes"""
       
   398         value = self.cw_attr_cache.setdefault(attr, default)
       
   399         # don't add attribute into skip_security if already in edited
       
   400         # attributes, else we may accidentaly skip a desired security check
       
   401         if hasattr(self, 'edited_attributes') and \
       
   402                attr not in self.edited_attributes:
       
   403             self.edited_attributes.add(attr)
       
   404             self._cw_skip_security_attributes.add(attr)
       
   405         return value
       
   406 
       
   407     def pop(self, attr, default=_marker):
       
   408         """override pop to update self.edited_attributes on cleanup of
       
   409         undesired changes introduced in the entity's dict. See `__delitem__`
       
   410         """
       
   411         if default is _marker:
       
   412             value = self.cw_attr_cache.pop(attr)
       
   413         else:
       
   414             value = self.cw_attr_cache.pop(attr, default)
       
   415         if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
       
   416             self.edited_attributes.remove(attr)
       
   417         return value
       
   418 
       
   419     def update(self, values):
       
   420         """override update to update self.edited_attributes. See `__setitem__`
       
   421         """
       
   422         for attr, value in values.items():
       
   423             self[attr] = value # use self.__setitem__ implementation
       
   424 
   325 
   425     def cw_adapt_to(self, interface):
   326     def cw_adapt_to(self, interface):
   426         """return an adapter the entity to the given interface name.
   327         """return an adapter the entity to the given interface name.
   427 
   328 
   428         return None if it can not be adapted.
   329         return None if it can not be adapted.
   590             data = soup2xhtml(data, self._cw.encoding)
   491             data = soup2xhtml(data, self._cw.encoding)
   591         return data
   492         return data
   592 
   493 
   593     # entity cloning ##########################################################
   494     # entity cloning ##########################################################
   594 
   495 
   595     def cw_copy(self):
       
   596         thecopy = copy(self)
       
   597         thecopy.cw_attr_cache = copy(self.cw_attr_cache)
       
   598         thecopy._cw_related_cache = {}
       
   599         return thecopy
       
   600 
       
   601     def copy_relations(self, ceid): # XXX cw_copy_relations
   496     def copy_relations(self, ceid): # XXX cw_copy_relations
   602         """copy relations of the object with the given eid on this
   497         """copy relations of the object with the given eid on this
   603         object (this method is called on the newly created copy, and
   498         object (this method is called on the newly created copy, and
   604         ceid designates the original entity).
   499         ceid designates the original entity).
   605 
   500 
   680                 continue
   575                 continue
   681             # password retreival is blocked at the repository server level
   576             # password retreival is blocked at the repository server level
   682             rdef = rschema.rdef(self.e_schema, attrschema)
   577             rdef = rschema.rdef(self.e_schema, attrschema)
   683             if not self._cw.user.matching_groups(rdef.get_groups('read')) \
   578             if not self._cw.user.matching_groups(rdef.get_groups('read')) \
   684                    or (attrschema.type == 'Password' and skip_pwd):
   579                    or (attrschema.type == 'Password' and skip_pwd):
   685                 self[attr] = None
   580                 self.cw_attr_cache[attr] = None
   686                 continue
   581                 continue
   687             yield attr
   582             yield attr
   688 
   583 
   689     _cw_completed = False
   584     _cw_completed = False
   690     def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
   585     def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
   736         if selected:
   631         if selected:
   737             # select V, we need it as the left most selected variable
   632             # select V, we need it as the left most selected variable
   738             # if some outer join are included to fetch inlined relations
   633             # if some outer join are included to fetch inlined relations
   739             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
   634             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
   740                                     ','.join(rql))
   635                                     ','.join(rql))
   741             rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
   636             try:
       
   637                 rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
       
   638             except IndexError:
       
   639                 raise Exception('unable to fetch attributes for entity with eid %s'
       
   640                                 % self.eid)
   742             # handle attributes
   641             # handle attributes
   743             for i in xrange(1, lastattr):
   642             for i in xrange(1, lastattr):
   744                 self[str(selected[i-1][0])] = rset[i]
   643                 self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
   745             # handle relations
   644             # handle relations
   746             for i in xrange(lastattr, len(rset)):
   645             for i in xrange(lastattr, len(rset)):
   747                 rtype, role = selected[i-1][0]
   646                 rtype, role = selected[i-1][0]
   748                 value = rset[i]
   647                 value = rset[i]
   749                 if value is None:
   648                 if value is None:
   759 
   658 
   760         :type name: str
   659         :type name: str
   761         :param name: name of the attribute to get
   660         :param name: name of the attribute to get
   762         """
   661         """
   763         try:
   662         try:
   764             value = self.cw_attr_cache[name]
   663             return self.cw_attr_cache[name]
   765         except KeyError:
   664         except KeyError:
   766             if not self.cw_is_saved():
   665             if not self.cw_is_saved():
   767                 return None
   666                 return None
   768             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
   667             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
   769             try:
   668             try:
   770                 rset = self._cw.execute(rql, {'x': self.eid})
   669                 rset = self._cw.execute(rql, {'x': self.eid})
   771             except Unauthorized:
   670             except Unauthorized:
   772                 self[name] = value = None
   671                 self.cw_attr_cache[name] = value = None
   773             else:
   672             else:
   774                 assert rset.rowcount <= 1, (self, rql, rset.rowcount)
   673                 assert rset.rowcount <= 1, (self, rql, rset.rowcount)
   775                 try:
   674                 try:
   776                     self[name] = value = rset.rows[0][0]
   675                     self.cw_attr_cache[name] = value = rset.rows[0][0]
   777                 except IndexError:
   676                 except IndexError:
   778                     # probably a multisource error
   677                     # probably a multisource error
   779                     self.critical("can't get value for attribute %s of entity with eid %s",
   678                     self.critical("can't get value for attribute %s of entity with eid %s",
   780                                   name, self.eid)
   679                                   name, self.eid)
   781                     if self.e_schema.destination(name) == 'String':
   680                     if self.e_schema.destination(name) == 'String':
   782                         # XXX (syt) imo emtpy string is better
   681                         self.cw_attr_cache[name] = value = self._cw._('unaccessible')
   783                         self[name] = value = self._cw._('unaccessible')
       
   784                     else:
   682                     else:
   785                         self[name] = value = None
   683                         self.cw_attr_cache[name] = value = None
   786         return value
   684             return value
   787 
   685 
   788     def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
   686     def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
   789         """returns a resultset of related entities
   687         """returns a resultset of related entities
   790 
   688 
   791         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   689         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   797         except KeyError:
   695         except KeyError:
   798             pass
   696             pass
   799         if not self.has_eid():
   697         if not self.has_eid():
   800             if entities:
   698             if entities:
   801                 return []
   699                 return []
   802             return self.empty_rset()
   700             return self._cw.empty_rset()
   803         rql = self.cw_related_rql(rtype, role)
   701         rql = self.cw_related_rql(rtype, role)
   804         rset = self._cw.execute(rql, {'x': self.eid})
   702         rset = self._cw.execute(rql, {'x': self.eid})
   805         self.cw_set_relation_cache(rtype, role, rset)
   703         self.cw_set_relation_cache(rtype, role, rset)
   806         return self.related(rtype, role, limit, entities)
   704         return self.related(rtype, role, limit, entities)
   807 
   705 
   985 
   883 
   986         If you use custom caches on your entity class (take care to @cached!),
   884         If you use custom caches on your entity class (take care to @cached!),
   987         you should override this method to clear them as well.
   885         you should override this method to clear them as well.
   988         """
   886         """
   989         # clear attributes cache
   887         # clear attributes cache
   990         haseid = 'eid' in self
       
   991         self._cw_completed = False
   888         self._cw_completed = False
   992         self.cw_attr_cache.clear()
   889         self.cw_attr_cache.clear()
   993         # clear relations cache
   890         # clear relations cache
   994         self.cw_clear_relation_cache()
   891         self.cw_clear_relation_cache()
   995         # rest path unique cache
   892         # rest path unique cache
  1012         kwargs['x'] = self.eid
   909         kwargs['x'] = self.eid
  1013         self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
   910         self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
  1014                          kwargs)
   911                          kwargs)
  1015         kwargs.pop('x')
   912         kwargs.pop('x')
  1016         # update current local object _after_ the rql query to avoid
   913         # update current local object _after_ the rql query to avoid
  1017         # interferences between the query execution itself and the
   914         # interferences between the query execution itself and the cw_edited /
  1018         # edited_attributes / skip_security_attributes machinery
   915         # skip_security machinery
  1019         self.update(kwargs)
   916         self.cw_attr_cache.update(kwargs)
  1020 
   917 
  1021     def set_relations(self, **kwargs): # XXX cw_set_relations
   918     def set_relations(self, **kwargs): # XXX cw_set_relations
  1022         """add relations to the given object. To set a relation where this entity
   919         """add relations to the given object. To set a relation where this entity
  1023         is the object of the relation, use 'reverse_'<relation> as argument name.
   920         is the object of the relation, use 'reverse_'<relation> as argument name.
  1024 
   921 
  1045     def cw_delete(self, **kwargs):
   942     def cw_delete(self, **kwargs):
  1046         assert self.has_eid(), self.eid
   943         assert self.has_eid(), self.eid
  1047         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
   944         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
  1048                          {'x': self.eid}, **kwargs)
   945                          {'x': self.eid}, **kwargs)
  1049 
   946 
  1050     # server side utilities ###################################################
   947     # server side utilities ####################################################
  1051 
       
  1052     def _cw_rql_set_value(self, attr, value):
       
  1053         """call by rql execution plan when some attribute is modified
       
  1054 
       
  1055         don't use dict api in such case since we don't want attribute to be
       
  1056         added to skip_security_attributes.
       
  1057 
       
  1058         This method is for internal use, you should not use it.
       
  1059         """
       
  1060         self.cw_attr_cache[attr] = value
       
  1061 
   948 
  1062     def _cw_clear_local_perm_cache(self, action):
   949     def _cw_clear_local_perm_cache(self, action):
  1063         for rqlexpr in self.e_schema.get_rqlexprs(action):
   950         for rqlexpr in self.e_schema.get_rqlexprs(action):
  1064             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
   951             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
  1065 
   952 
  1066     @property
   953     # deprecated stuff #########################################################
  1067     def _cw_skip_security_attributes(self):
       
  1068         try:
       
  1069             return self.__cw_skip_security_attributes
       
  1070         except:
       
  1071             self.__cw_skip_security_attributes = set()
       
  1072             return self.__cw_skip_security_attributes
       
  1073 
       
  1074     def _cw_set_defaults(self):
       
  1075         """set default values according to the schema"""
       
  1076         for attr, value in self.e_schema.defaults():
       
  1077             if not self.cw_attr_cache.has_key(attr):
       
  1078                 self[str(attr)] = value
       
  1079 
       
  1080     def _cw_check(self, creation=False):
       
  1081         """check this entity against its schema. Only final relation
       
  1082         are checked here, constraint on actual relations are checked in hooks
       
  1083         """
       
  1084         # necessary since eid is handled specifically and yams require it to be
       
  1085         # in the dictionary
       
  1086         if self._cw is None:
       
  1087             _ = unicode
       
  1088         else:
       
  1089             _ = self._cw._
       
  1090         if creation:
       
  1091             # on creations, we want to check all relations, especially
       
  1092             # required attributes
       
  1093             relations = [rschema for rschema in self.e_schema.subject_relations()
       
  1094                          if rschema.final and rschema.type != 'eid']
       
  1095         elif hasattr(self, 'edited_attributes'):
       
  1096             relations = [self._cw.vreg.schema.rschema(rtype)
       
  1097                          for rtype in self.edited_attributes]
       
  1098         else:
       
  1099             relations = None
       
  1100         self.e_schema.check(self, creation=creation, _=_,
       
  1101                             relations=relations)
       
  1102 
   954 
  1103     @deprecated('[3.9] use entity.cw_attr_value(attr)')
   955     @deprecated('[3.9] use entity.cw_attr_value(attr)')
  1104     def get_value(self, name):
   956     def get_value(self, name):
  1105         return self.cw_attr_value(name)
   957         return self.cw_attr_value(name)
  1106 
   958 
  1118 
   970 
  1119     @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)')
   971     @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)')
  1120     def set_related_cache(self, rtype, role, rset):
   972     def set_related_cache(self, rtype, role, rset):
  1121         self.cw_set_relation_cache(rtype, role, rset)
   973         self.cw_set_relation_cache(rtype, role, rset)
  1122 
   974 
  1123     @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role, rset)')
   975     @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role)')
  1124     def clear_related_cache(self, rtype=None, role=None):
   976     def clear_related_cache(self, rtype=None, role=None):
  1125         self.cw_clear_relation_cache(rtype, role)
   977         self.cw_clear_relation_cache(rtype, role)
  1126 
   978 
  1127     @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])')
   979     @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])')
  1128     def related_rql(self, rtype, role='subject', targettypes=None):
   980     def related_rql(self, rtype, role='subject', targettypes=None):
  1129         return self.cw_related_rql(rtype, role, targettypes)
   981         return self.cw_related_rql(rtype, role, targettypes)
       
   982 
       
   983     @property
       
   984     @deprecated('[3.10] use entity.cw_edited')
       
   985     def edited_attributes(self):
       
   986         return self.cw_edited
       
   987 
       
   988     @property
       
   989     @deprecated('[3.10] use entity.cw_edited.skip_security')
       
   990     def skip_security_attributes(self):
       
   991         return self.cw_edited.skip_security
       
   992 
       
   993     @property
       
   994     @deprecated('[3.10] use entity.cw_edited.skip_security')
       
   995     def _cw_skip_security_attributes(self):
       
   996         return self.cw_edited.skip_security
       
   997 
       
   998     @property
       
   999     @deprecated('[3.10] use entity.cw_edited.querier_pending_relations')
       
  1000     def querier_pending_relations(self):
       
  1001         return self.cw_edited.querier_pending_relations
       
  1002 
       
  1003     @deprecated('[3.10] use key in entity.cw_attr_cache')
       
  1004     def __contains__(self, key):
       
  1005         return key in self.cw_attr_cache
       
  1006 
       
  1007     @deprecated('[3.10] iter on entity.cw_attr_cache')
       
  1008     def __iter__(self):
       
  1009         return iter(self.cw_attr_cache)
       
  1010 
       
  1011     @deprecated('[3.10] use entity.cw_attr_cache[attr]')
       
  1012     def __getitem__(self, key):
       
  1013         if key == 'eid':
       
  1014             warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
       
  1015                  DeprecationWarning, stacklevel=2)
       
  1016             return self.eid
       
  1017         return self.cw_attr_cache[key]
       
  1018 
       
  1019     @deprecated('[3.10] use entity.cw_attr_cache.get(attr[, default])')
       
  1020     def get(self, key, default=None):
       
  1021         return self.cw_attr_cache.get(key, default)
       
  1022 
       
  1023     @deprecated('[3.10] use entity.cw_attr_cache.clear()')
       
  1024     def clear(self):
       
  1025         self.cw_attr_cache.clear()
       
  1026         # XXX clear cw_edited ?
       
  1027 
       
  1028     @deprecated('[3.10] use entity.cw_edited[attr] = value or entity.cw_attr_cache[attr] = value')
       
  1029     def __setitem__(self, attr, value):
       
  1030         """override __setitem__ to update self.cw_edited.
       
  1031 
       
  1032         Typically, a before_[update|add]_hook could do::
       
  1033 
       
  1034             entity['generated_attr'] = generated_value
       
  1035 
       
  1036         and this way, cw_edited will be updated accordingly. Also, add
       
  1037         the attribute to skip_security since we don't want to check security
       
  1038         for such attributes set by hooks.
       
  1039         """
       
  1040         if attr == 'eid':
       
  1041             warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
       
  1042                  DeprecationWarning, stacklevel=2)
       
  1043             self.eid = value
       
  1044         else:
       
  1045             try:
       
  1046                 self.cw_edited[attr] = value
       
  1047             except AttributeError:
       
  1048                 self.cw_attr_cache[attr] = value
       
  1049 
       
  1050     @deprecated('[3.10] use del entity.cw_edited[attr]')
       
  1051     def __delitem__(self, attr):
       
  1052         """override __delitem__ to update self.cw_edited on cleanup of
       
  1053         undesired changes introduced in the entity's dict. For example, see the
       
  1054         code snippet below from the `forge` cube:
       
  1055 
       
  1056         .. sourcecode:: python
       
  1057 
       
  1058             edited = self.entity.cw_edited
       
  1059             has_load_left = 'load_left' in edited
       
  1060             if 'load' in edited and self.entity.load_left is None:
       
  1061                 self.entity.load_left = self.entity['load']
       
  1062             elif not has_load_left and edited:
       
  1063                 # cleanup, this may cause undesired changes
       
  1064                 del self.entity['load_left']
       
  1065         """
       
  1066         del self.cw_edited[attr]
       
  1067 
       
  1068     @deprecated('[3.10] use entity.cw_edited.setdefault(attr, default)')
       
  1069     def setdefault(self, attr, default):
       
  1070         """override setdefault to update self.cw_edited"""
       
  1071         return self.cw_edited.setdefault(attr, default)
       
  1072 
       
  1073     @deprecated('[3.10] use entity.cw_edited.pop(attr[, default])')
       
  1074     def pop(self, attr, *args):
       
  1075         """override pop to update self.cw_edited on cleanup of
       
  1076         undesired changes introduced in the entity's dict. See `__delitem__`
       
  1077         """
       
  1078         return self.cw_edited.pop(attr, *args)
       
  1079 
       
  1080     @deprecated('[3.10] use entity.cw_edited.update(values)')
       
  1081     def update(self, values):
       
  1082         """override update to update self.cw_edited. See `__setitem__`
       
  1083         """
       
  1084         self.cw_edited.update(values)
  1130 
  1085 
  1131 
  1086 
  1132 # attribute and relation descriptors ##########################################
  1087 # attribute and relation descriptors ##########################################
  1133 
  1088 
  1134 class Attribute(object):
  1089 class Attribute(object):
  1141     def __get__(self, eobj, eclass):
  1096     def __get__(self, eobj, eclass):
  1142         if eobj is None:
  1097         if eobj is None:
  1143             return self
  1098             return self
  1144         return eobj.cw_attr_value(self._attrname)
  1099         return eobj.cw_attr_value(self._attrname)
  1145 
  1100 
       
  1101     @deprecated('[3.10] assign to entity.cw_attr_cache[attr] or entity.cw_edited[attr]')
  1146     def __set__(self, eobj, value):
  1102     def __set__(self, eobj, value):
  1147         eobj[self._attrname] = value
  1103         if hasattr(eobj, 'cw_edited') and not eobj.cw_edited.saved:
       
  1104             eobj.cw_edited[self._attrname] = value
       
  1105         else:
       
  1106             eobj.cw_attr_cache[self._attrname] = value
  1148 
  1107 
  1149 
  1108 
  1150 class Relation(object):
  1109 class Relation(object):
  1151     """descriptor that controls schema relation access"""
  1110     """descriptor that controls schema relation access"""
  1152 
  1111