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

zopefoundation / zc.relationship / 6683185508

09 Feb 2023 01:24PM UTC coverage: 94.101% (+0.4%) from 93.671%
6683185508

push

github

web-flow
Config with pure python template a07f4b66 (#11)

* Bumped version for breaking release.

* Drop support for Python 2.7, 3.5, 3.6.

* Add support for Python 3.10, 3.11.

Co-authored-by: Jens Vagelpohl <jens@plyp.com>

150 of 173 branches covered (0.0%)

Branch coverage included in aggregate %.

49 of 49 new or added lines in 7 files covered. (100.0%)

520 of 539 relevant lines covered (96.47%)

0.96 hits per line

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

87.75
/src/zc/relationship/shared.py
1
##############################################################################
2
#
3
# Copyright (c) 2006 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
"""Relationship shared code."""
1✔
15
import random
1✔
16

17
import persistent
1✔
18
import zope.app.container.btree
1✔
19
import zope.app.container.contained
1✔
20
from zope import interface
1✔
21

22
from zc.relationship import index
1✔
23
from zc.relationship import interfaces
1✔
24

25

26
##############################################################################
27
# some useful relationship variants
28
#
29

30
try:
1✔
31
    import zc.listcontainer
1✔
32
except ImportError:
33
    class RelationshipBase(
34
            persistent.Persistent, zope.app.container.contained.Contained):
35
        pass
36
else:
37
    class RelationshipBase(
×
38
            persistent.Persistent,
39
            zope.app.container.contained.Contained,
40
            zc.listcontainer.Contained):
41
        pass
×
42

43

44
def apply(func, *args, **kw):
1✔
45
    return func(*args, **kw)
1✔
46

47

48
@interface.implementer(interfaces.IRelationship)
1✔
49
class ImmutableRelationship(RelationshipBase):
1✔
50

51
    _marker = __name__ = __parent__ = None
1✔
52

53
    def __init__(self, sources, targets):
1✔
54
        self._sources = tuple(sources)
1✔
55
        self._targets = tuple(targets)
1✔
56

57
    @property
1✔
58
    def sources(self):
1✔
59
        return self._sources
1✔
60

61
    @property
1✔
62
    def targets(self):
1✔
63
        return self._targets
1✔
64

65
    def __repr__(self):
1✔
66
        return f'<Relationship from {self.sources!r} to {self.targets!r}>'
1✔
67

68

69
@interface.implementer(interfaces.IMutableRelationship)
1✔
70
class Relationship(ImmutableRelationship):
1✔
71

72
    @apply
1✔
73
    def sources():
1✔
74
        def get(self):
1✔
75
            return self._sources
1✔
76

77
        def set(self, value):
1✔
78
            self._sources = tuple(value)
1✔
79
            if interfaces.IBidirectionalRelationshipIndex.providedBy(
1!
80
                    self.__parent__):
81
                self.__parent__.reindex(self)
1✔
82
        return property(get, set)
1✔
83

84
    @apply
1✔
85
    def targets():
1✔
86
        def get(self):
1✔
87
            return self._targets
1✔
88

89
        def set(self, value):
1✔
90
            self._targets = tuple(value)
1✔
91
            if interfaces.IBidirectionalRelationshipIndex.providedBy(
1!
92
                    self.__parent__):
93
                self.__parent__.reindex(self)
1✔
94
        return property(get, set)
1✔
95

96
# some small conveniences; maybe overkill, but I wanted some for a client
97
# package.
98

99

100
@interface.implementer(interfaces.IOneToOneRelationship)
1✔
101
class OneToOneRelationship(ImmutableRelationship):
1✔
102

103
    def __init__(self, source, target):
1✔
104
        super().__init__((source,), (target,))
1✔
105

106
    @apply
1✔
107
    def source():
1✔
108
        def get(self):
1✔
109
            return self._sources[0]
1✔
110

111
        def set(self, value):
1✔
112
            self._sources = (value,)
1✔
113
            if interfaces.IBidirectionalRelationshipIndex.providedBy(
1!
114
                    self.__parent__):
115
                self.__parent__.reindex(self)
1✔
116
        return property(get, set)
1✔
117

118
    @apply
1✔
119
    def target():
1✔
120
        def get(self):
1✔
121
            return self._targets[0]
1✔
122

123
        def set(self, value):
1✔
124
            self._targets = (value,)
1✔
125
            if interfaces.IBidirectionalRelationshipIndex.providedBy(
1!
126
                    self.__parent__):
127
                self.__parent__.reindex(self)
1✔
128
        return property(get, set)
1✔
129

130

131
@interface.implementer(interfaces.IOneToManyRelationship)
1✔
132
class OneToManyRelationship(ImmutableRelationship):
1✔
133

134
    def __init__(self, source, targets):
1✔
135
        super().__init__((source,), targets)
1✔
136

137
    @apply
1✔
138
    def source():
1✔
139
        def get(self):
1✔
140
            return self._sources[0]
1✔
141

142
        def set(self, value):
1✔
143
            self._sources = (value,)
×
144
            if interfaces.IBidirectionalRelationshipIndex.providedBy(
×
145
                    self.__parent__):
146
                self.__parent__.reindex(self)
×
147
        return property(get, set)
1✔
148

149
    @apply
1✔
150
    def targets():
1✔
151
        def get(self):
1✔
152
            return self._targets
1✔
153

154
        def set(self, value):
1✔
155
            self._targets = tuple(value)
1✔
156
            if interfaces.IBidirectionalRelationshipIndex.providedBy(
1!
157
                    self.__parent__):
158
                self.__parent__.reindex(self)
1✔
159
        return property(get, set)
1✔
160

161

162
@interface.implementer(interfaces.IManyToOneRelationship)
1✔
163
class ManyToOneRelationship(ImmutableRelationship):
1✔
164

165
    def __init__(self, sources, target):
1✔
166
        super().__init__(sources, (target,))
1✔
167

168
    @apply
1✔
169
    def sources():
1✔
170
        def get(self):
1✔
171
            return self._sources
1✔
172

173
        def set(self, value):
1✔
174
            self._sources = tuple(value)
1✔
175
            if interfaces.IBidirectionalRelationshipIndex.providedBy(
1!
176
                    self.__parent__):
177
                self.__parent__.reindex(self)
1✔
178
        return property(get, set)
1✔
179

180
    @apply
1✔
181
    def target():
1✔
182
        def get(self):
1✔
183
            return self._targets[0]
1✔
184

185
        def set(self, value):
1✔
186
            self._targets = (value,)
1✔
187
            if interfaces.IBidirectionalRelationshipIndex.providedBy(
1!
188
                    self.__parent__):
189
                self.__parent__.reindex(self)
1✔
190
        return property(get, set)
1✔
191

192
##############################################################################
193

194

195
class ResolvingFilter:
1✔
196
    def __init__(self, filter, container):
1✔
197
        self.filter = filter
1✔
198
        self.container = container
1✔
199

200
    def __call__(self, relchain, query, index, cache):
1✔
201
        obj = self.container.relationIndex.resolveRelationshipToken(
1✔
202
            relchain[-1])
203
        return self.filter(obj)
1✔
204

205

206
def minDepthFilter(depth):
1✔
207
    if depth is None:
1!
208
        return None
1✔
209
    if not isinstance(depth, int) or depth < 1:
×
210
        raise ValueError('invalid minDepth', depth)
×
211
    return lambda relchain, query, index, cache: len(relchain) >= depth
×
212

213

214
class AbstractContainer(persistent.Persistent):
1✔
215
    def __init__(self,
1✔
216
                 dumpSource=None, loadSource=None, sourceFamily=None,
217
                 dumpTarget=None, loadTarget=None, targetFamily=None,
218
                 **kwargs):
219
        source = {'element': interfaces.IRelationship['sources'],
1✔
220
                  'name': 'source', 'multiple': True}
221
        target = {'element': interfaces.IRelationship['targets'],
1✔
222
                  'name': 'target', 'multiple': True}
223
        if dumpSource is not None:
1✔
224
            target['dump'] = source['dump'] = dumpSource
1✔
225
        if loadSource is not None:
1✔
226
            target['load'] = source['load'] = loadSource
1✔
227
        if sourceFamily is not None:
1✔
228
            target['btree'] = source['btree'] = sourceFamily
1✔
229
        if dumpTarget is not None:
1!
230
            target['dump'] = dumpTarget
×
231
        if loadTarget is not None:
1!
232
            target['load'] = loadTarget
×
233
        if targetFamily is not None:
1!
234
            target['btree'] = targetFamily
×
235

236
        ix = index.Index(
1✔
237
            (source, target),
238
            index.TransposingTransitiveQueriesFactory('source', 'target'),
239
            **kwargs)
240
        self.relationIndex = ix
1✔
241
        ix.__parent__ = self
1✔
242

243
    def reindex(self, object):
1✔
244
        assert object.__parent__ is self
1✔
245
        self.relationIndex.index(object)
1✔
246

247
    def findTargets(self, source, maxDepth=1, minDepth=None, filter=None):
1✔
248
        return self.relationIndex.findValues(
1✔
249
            'target', self.relationIndex.tokenizeQuery({'source': source}),
250
            maxDepth, filter and ResolvingFilter(filter, self),
251
            targetFilter=minDepthFilter(minDepth))
252

253
    def findSources(self, target, maxDepth=1, minDepth=None, filter=None):
1✔
254
        return self.relationIndex.findValues(
1✔
255
            'source', self.relationIndex.tokenizeQuery({'target': target}),
256
            maxDepth, filter and ResolvingFilter(filter, self),
257
            targetFilter=minDepthFilter(minDepth))
258

259
    def findTargetTokens(self, source, maxDepth=1, minDepth=None, filter=None):
1✔
260
        return self.relationIndex.findValueTokens(
×
261
            'target', self.relationIndex.tokenizeQuery({'source': source}),
262
            maxDepth, filter and ResolvingFilter(filter, self),
263
            targetFilter=minDepthFilter(minDepth))
264

265
    def findSourceTokens(self, target, maxDepth=1, minDepth=None, filter=None):
1✔
266
        return self.relationIndex.findValueTokens(
×
267
            'source', self.relationIndex.tokenizeQuery({'target': target}),
268
            maxDepth, filter and ResolvingFilter(filter, self),
269
            targetFilter=minDepthFilter(minDepth))
270

271
    def isLinked(self, source=None, target=None, maxDepth=1, minDepth=None,
1✔
272
                 filter=None):
273
        tokenize = self.relationIndex.tokenizeQuery
1✔
274
        if source is not None:
1✔
275
            if target is not None:
1✔
276
                targetQuery = tokenize({'target': target})
1✔
277
            else:
278
                targetQuery = None
1✔
279
            return self.relationIndex.isLinked(
1✔
280
                tokenize({'source': source}),
281
                maxDepth, filter and ResolvingFilter(filter, self),
282
                targetQuery,
283
                targetFilter=minDepthFilter(minDepth))
284
        elif target is not None:
1!
285
            return self.relationIndex.isLinked(
1✔
286
                tokenize({'target': target}),
287
                maxDepth, filter and ResolvingFilter(filter, self),
288
                targetFilter=minDepthFilter(minDepth))
289
        else:
290
            raise ValueError(
×
291
                'at least one of `source` and `target` must be provided')
292

293
    def _reverse(self, iterable):
1✔
294
        for i in iterable:
1✔
295
            if interfaces.ICircularRelationshipPath.providedBy(i):
1✔
296
                yield index.CircularRelationshipPath(
1✔
297
                    reversed(i),
298
                    [self.relationIndex.resolveQuery(d) for d in i.cycled])
299
            else:
300
                yield tuple(reversed(i))
1✔
301

302
    def _forward(self, iterable):
1✔
303
        for i in iterable:
1✔
304
            if interfaces.ICircularRelationshipPath.providedBy(i):
1✔
305
                yield index.CircularRelationshipPath(
1✔
306
                    i,
307
                    [self.relationIndex.resolveQuery(d) for d in i.cycled])
308
            else:
309
                yield i
1✔
310

311
    def findRelationshipTokens(self, source=None, target=None, maxDepth=1,
1✔
312
                               minDepth=None, filter=None):
313
        tokenize = self.relationIndex.tokenizeQuery
1✔
314
        if source is not None:
1✔
315
            if target is not None:
1✔
316
                targetQuery = tokenize({'target': target})
1✔
317
            else:
318
                targetQuery = None
1✔
319
            res = self.relationIndex.findRelationshipTokenChains(
1✔
320
                tokenize({'source': source}),
321
                maxDepth, filter and ResolvingFilter(filter, self),
322
                targetQuery,
323
                targetFilter=minDepthFilter(minDepth))
324
            return self._forward(res)
1✔
325
        elif target is not None:
1✔
326
            res = self.relationIndex.findRelationshipTokenChains(
1✔
327
                tokenize({'target': target}),
328
                maxDepth, filter and ResolvingFilter(filter, self),
329
                targetFilter=minDepthFilter(minDepth))
330
            return self._reverse(res)
1✔
331
        else:
332
            raise ValueError(
1✔
333
                'at least one of `source` and `target` must be provided')
334

335
    def _resolveRelationshipChains(self, iterable):
1✔
336
        for i in iterable:
1✔
337
            chain = tuple(self.relationIndex.resolveRelationshipTokens(i))
1✔
338
            if interfaces.ICircularRelationshipPath.providedBy(i):
1✔
339
                yield index.CircularRelationshipPath(chain, i.cycled)
1✔
340
            else:
341
                yield tuple(chain)
1✔
342

343
    def findRelationships(self, source=None, target=None, maxDepth=1,
1✔
344
                          minDepth=None, filter=None):
345
        return self._resolveRelationshipChains(
1✔
346
            self.findRelationshipTokens(
347
                source, target, maxDepth, minDepth, filter))
348

349

350
class Container(AbstractContainer, zope.app.container.btree.BTreeContainer):
1✔
351

352
    def __init__(self, *args, **kwargs):
1✔
353
        AbstractContainer.__init__(self, *args, **kwargs)
1✔
354
        zope.app.container.btree.BTreeContainer.__init__(self)
1✔
355

356
    # subclass API
357
    def _generate_id(self, relationship):
1✔
358
        return ''.join(random.sample(
1✔
359
            "abcdefghijklmnopqrtstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_1234567890",
360
            30))  # somewhat less than 64 ** 30 variations (64*63*...*35)
361
    # end subclass API
362

363
    def add(self, object):
1✔
364
        key = self._generate_id(object)
1✔
365
        while key in self._SampleContainer__data:
1!
366
            key = self._generate_id(object)
×
367
        super(AbstractContainer, self).__setitem__(key, object)
1✔
368
        self.relationIndex.index(object)
1✔
369

370
    def remove(self, object):
1✔
371
        key = object.__name__
1✔
372
        if self[key] is not object:
1!
373
            raise ValueError("Relationship is not stored as its __name__")
×
374
        self.relationIndex.unindex(object)
1✔
375
        super(AbstractContainer, self).__delitem__(key)
1✔
376

377
    @property
1✔
378
    def __setitem__(self):
1✔
379
        raise AttributeError
×
380
    __delitem__ = __setitem__
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