• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / ZODB / 18153960591

01 Oct 2025 06:50AM UTC coverage: 83.781% (-0.03%) from 83.811%
18153960591

Pull #415

github

web-flow
Update docs/articles/old-guide/convert_zodb_guide.py

Co-authored-by: Michael Howitz <icemac@gmx.net>
Pull Request #415: Apply the latest zope.meta templates

2441 of 3542 branches covered (68.92%)

193 of 257 new or added lines in 48 files covered. (75.1%)

12 existing lines in 6 files now uncovered.

13353 of 15938 relevant lines covered (83.78%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.91
/src/ZODB/ConflictResolution.py
1
##############################################################################
2
#
3
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE
12
#
13
##############################################################################
14

15
import logging
1✔
16
from io import BytesIO
1✔
17
from pickle import PicklingError
1✔
18

19
import zope.interface
1✔
20

21
from ZODB._compat import PersistentPickler
1✔
22
from ZODB._compat import PersistentUnpickler
1✔
23
from ZODB._compat import _protocol
1✔
24
from ZODB.loglevels import BLATHER
1✔
25
from ZODB.POSException import ConflictError
1✔
26

27

28
logger = logging.getLogger('ZODB.ConflictResolution')
1✔
29

30

31
class BadClassName(Exception):
1✔
32
    pass
1✔
33

34

35
class BadClass:
1✔
36

37
    def __init__(self, *args):
1✔
38
        self.args = args
1✔
39

40
    def __reduce__(self):
1✔
41
        raise BadClassName(*self.args)
×
42

43

44
_class_cache = {}
1✔
45
_class_cache_get = _class_cache.get
1✔
46

47

48
def find_global(*args):
1✔
49
    cls = _class_cache_get(args, 0)
1✔
50
    if cls == 0:
1✔
51
        # Not cached. Try to import
52
        try:
1✔
53
            module = __import__(args[0], {}, {}, ['cluck'])
1✔
54
        except ImportError:
55
            cls = 1
56
        else:
57
            cls = getattr(module, args[1], 1)
1✔
58
        _class_cache[args] = cls
1✔
59

60
        if cls == 1:
1✔
61
            logger.log(BLATHER, "Unable to load class", exc_info=True)
1✔
62

63
    if cls == 1:
1✔
64
        # Not importable
65
        if (isinstance(args, tuple) and len(args) == 2 and
1✔
66
                isinstance(args[0], str) and
67
                isinstance(args[1], str)):
68
            return BadClass(*args)
1✔
69
        else:
70
            raise BadClassName(*args)
1✔
71
    return cls
1✔
72

73

74
def state(self, oid, serial, prfactory, p=''):
1✔
75
    p = p or self.loadSerial(oid, serial)
1✔
76
    p = self._crs_untransform_record_data(p)
1✔
77
    file = BytesIO(p)
1✔
78
    unpickler = PersistentUnpickler(
1✔
79
        find_global, prfactory.persistent_load, file)
80
    unpickler.load()  # skip the class tuple
1✔
81
    return unpickler.load()
1✔
82

83

84
class IPersistentReference(zope.interface.Interface):
1✔
85
    '''public contract for references to persistent objects from an object
86
    with conflicts.'''
87

88
    oid = zope.interface.Attribute(
1✔
89
        'The oid of the persistent object that this reference represents')
90

91
    database_name = zope.interface.Attribute(
1✔
92
        '''The name of the database of the reference, *if* different.
93

94
        If not different, None.''')
95

96
    klass = zope.interface.Attribute(
1✔
97
        '''class meta data.  Presence is not reliable.''')
98

99
    weak = zope.interface.Attribute(
1✔
100
        '''bool: whether this reference is weak''')
101

102
    def __cmp__(other):
1✔
103
        '''if other is equivalent reference, return 0; else raise ValueError.
104

105
        Equivalent in this case means that oid and database_name are the same.
106

107
        If either is a weak reference, we only support `is` equivalence, and
108
        otherwise raise a ValueError even if the datbase_names and oids are
109
        the same, rather than guess at the correct semantics.
110

111
        It is impossible to sort reliably, since the actual persistent
112
        class may have its own comparison, and we have no idea what it is.
113
        We assert that it is reasonably safe to assume that an object is
114
        equivalent to itself, but that's as much as we can say.
115

116
        We don't compare on 'is other', despite the
117
        PersistentReferenceFactory.data cache, because it is possible to
118
        have two references to the same object that are spelled with different
119
        data (for instance, one with a class and one without).'''
120

121

122
@zope.interface.implementer(IPersistentReference)
1✔
123
class PersistentReference:
1✔
124

125
    weak = False
1✔
126
    oid = database_name = None
1✔
127

128
    def __init__(self, data):
1✔
129
        self.data = data
1✔
130
        # see serialize.py, ObjectReader._persistent_load
131
        if isinstance(data, tuple):
1✔
132
            self.oid, klass = data
1✔
133
            if isinstance(klass, BadClass):
1✔
134
                # We can't use the BadClass directly because, if
135
                # resolution succeeds, there's no good way to pickle
136
                # it.  Fortunately, a class reference in a persistent
137
                # reference is allowed to be a module+name tuple.
138
                self.data = self.oid, klass.args
1✔
139
        elif isinstance(data, (bytes, str)):
1✔
140
            self.oid = data
1✔
141
        else:  # a list
142
            reference_type = data[0]
1✔
143
            # 'm' = multi_persistent: (database_name, oid, klass)
144
            # 'n' = multi_oid: (database_name, oid)
145
            # 'w' = persistent weakref: (oid)
146
            #    or persistent weakref: (oid, database_name)
147
            # else it is a weakref: reference_type
148
            if reference_type == 'm':
1✔
149
                self.database_name, self.oid, klass = data[1]
1✔
150
                if isinstance(klass, BadClass):
1✔
151
                    # see above wrt BadClass
152
                    data[1] = self.database_name, self.oid, klass.args
1✔
153
            elif reference_type == 'n':
1✔
154
                self.database_name, self.oid = data[1]
1✔
155
            elif reference_type == 'w':
1✔
156
                try:
1✔
157
                    self.oid, = data[1]
1✔
158
                except ValueError:
1✔
159
                    self.oid, self.database_name = data[1]
1✔
160
                self.weak = True
1✔
161
            else:
162
                assert len(data) == 1, 'unknown reference format'
1✔
163
                self.oid = data[0]
1✔
164
                self.weak = True
1✔
165
        if not isinstance(self.oid, (bytes, type(None))):
1✔
166
            assert isinstance(self.oid, str)
1✔
167
            # this happens when all bytes in the oid are < 0x80
168
            self.oid = self.oid.encode('ascii')
1✔
169

170
    def __cmp__(self, other):
1✔
171
        if self is other or (
1✔
172
                isinstance(other, PersistentReference) and
173
                self.oid == other.oid and
174
                self.database_name == other.database_name and
175
                not self.weak and
176
                not other.weak):
177
            return 0
1✔
178
        else:
179
            raise ValueError(
1✔
180
                "can't reliably compare against different "
181
                "PersistentReferences")
182

183
    def __eq__(self, other):
1✔
184
        return self.__cmp__(other) == 0
1✔
185

186
    def __ne__(self, other):
1✔
187
        return self.__cmp__(other) != 0
×
188

189
    def __lt__(self, other):
1✔
190
        return self.__cmp__(other) < 0
1✔
191

192
    def __gt__(self, other):
1✔
193
        return self.__cmp__(other) > 0
×
194

195
    def __le__(self, other):
1✔
196
        return self.__cmp__(other) <= 0
×
197

198
    def __ge__(self, other):
1✔
199
        return self.__cmp__(other) >= 0
×
200

201
    def __repr__(self):
1✔
NEW
202
        return f"PR({id(self)} {self.data})"
×
203

204
    def __getstate__(self):
1✔
205
        raise PicklingError("Can't pickle PersistentReference")
×
206

207
    @property
1✔
208
    def klass(self):
1✔
209
        # for tests
210
        data = self.data
1✔
211
        if isinstance(data, tuple):
1✔
212
            return data[1]
1✔
213
        elif isinstance(data, list) and data[0] == 'm':
1✔
214
            return data[1][2]
1✔
215

216

217
class PersistentReferenceFactory:
1✔
218

219
    data = None
1✔
220

221
    def persistent_load(self, ref):
1✔
222
        if self.data is None:
1✔
223
            self.data = {}
1✔
224
        # lists are not hashable; formats are different enough
225
        key = tuple(ref)
1✔
226
        # even after eliminating list/tuple distinction
227
        r = self.data.get(key, None)
1✔
228
        if r is None:
1✔
229
            r = PersistentReference(ref)
1✔
230
            self.data[key] = r
1✔
231

232
        return r
1✔
233

234

235
def persistent_id(object):
1✔
236
    if getattr(object, '__class__', 0) is not PersistentReference:
1✔
237
        return None
1✔
238
    return object.data
1✔
239

240

241
_unresolvable = {}
1✔
242

243

244
def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle,
1✔
245
                         committedData=b''):
246
    # class_tuple, old, committed, newstate = ('',''), 0, 0, 0
247
    klass = 'n/a'
1✔
248
    try:
1✔
249
        prfactory = PersistentReferenceFactory()
1✔
250
        newpickle = self._crs_untransform_record_data(newpickle)
1✔
251
        file = BytesIO(newpickle)
1✔
252
        unpickler = PersistentUnpickler(
1✔
253
            find_global, prfactory.persistent_load, file)
254
        meta = unpickler.load()
1✔
255
        if isinstance(meta, tuple):
1✔
256
            klass = meta[0]
1✔
257
            newargs = meta[1] or ()
1✔
258
            if isinstance(klass, tuple):
1!
259
                klass = find_global(*klass)
1✔
260
        else:
261
            klass = meta
1✔
262
            newargs = ()
1✔
263

264
        if klass in _unresolvable:
1✔
265
            raise ConflictError
1✔
266

267
        inst = klass.__new__(klass, *newargs)
1✔
268

269
        try:
1✔
270
            resolve = inst._p_resolveConflict
1✔
271
        except AttributeError:
1✔
272
            _unresolvable[klass] = 1
1✔
273
            raise ConflictError
1✔
274

275
        oldData = self.loadSerial(oid, oldSerial)
1✔
276
        if not committedData:
1✔
277
            committedData = self.loadSerial(oid, committedSerial)
1✔
278

279
        newstate = unpickler.load()
1✔
280
        old = state(self, oid, oldSerial, prfactory, oldData)
1✔
281
        committed = state(self, oid, committedSerial, prfactory, committedData)
1✔
282

283
        resolved = resolve(old, committed, newstate)
1✔
284

285
        file = BytesIO()
1✔
286
        pickler = PersistentPickler(persistent_id, file, _protocol)
1✔
287
        pickler.dump(meta)
1✔
288
        pickler.dump(resolved)
1✔
289
        return self._crs_transform_record_data(file.getvalue())
1✔
290
    except (ConflictError, BadClassName) as e:
1✔
291
        logger.debug(
1✔
292
            "Conflict resolution on %s failed with %s: %s",
293
            klass, e.__class__.__name__, str(e))
294
    except:  # noqa: E722 do not use bare 'except'
1✔
295
        # If anything else went wrong, catch it here and avoid passing an
296
        # arbitrary exception back to the client.  The error here will mask
297
        # the original ConflictError.  A client can recover from a
298
        # ConflictError, but not necessarily from other errors.  But log
299
        # the error so that any problems can be fixed.
300
        logger.exception(
1✔
301
            "Unexpected error while trying to resolve conflict on %s", klass)
302

303
    raise ConflictError(oid=oid, serials=(committedSerial, oldSerial),
1✔
304
                        data=newpickle)
305

306

307
class ConflictResolvingStorage:
1✔
308
    "Mix-in class that provides conflict resolution handling for storages"
309

310
    tryToResolveConflict = tryToResolveConflict
1✔
311

312
    _crs_transform_record_data = _crs_untransform_record_data = (
1✔
313
        lambda self, o: o)
314

315
    def registerDB(self, wrapper):
1✔
316
        self._crs_untransform_record_data = wrapper.untransform_record_data
1✔
317
        self._crs_transform_record_data = wrapper.transform_record_data
1✔
318
        try:
1✔
319
            m = super().registerDB
1✔
320
        except AttributeError:
1✔
321
            pass
1✔
322
        else:
323
            m(wrapper)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc