comparison env/lib/python3.7/site-packages/rdflib/paths.py @ 5:9b1c78e6ba9c draft default tip

"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
author shellac
date Mon, 01 Jun 2020 08:59:25 -0400
parents 79f47841a781
children
comparison
equal deleted inserted replaced
4:79f47841a781 5:9b1c78e6ba9c
1 from rdflib.py3compat import PY3, format_doctest_out
2
3 __doc__ = format_doctest_out("""
4
5 This module implements the SPARQL 1.1 Property path operators, as
6 defined in:
7
8 http://www.w3.org/TR/sparql11-query/#propertypaths
9
10 In SPARQL the syntax is as follows:
11
12 +--------------------+-------------------------------------------------+
13 |Syntax | Matches |
14 +====================+=================================================+
15 |iri | An IRI. A path of length one. |
16 +--------------------+-------------------------------------------------+
17 |^elt | Inverse path (object to subject). |
18 +--------------------+-------------------------------------------------+
19 |elt1 / elt2 | A sequence path of elt1 followed by elt2. |
20 +--------------------+-------------------------------------------------+
21 |elt1 | elt2 | A alternative path of elt1 or elt2 |
22 | | (all possibilities are tried). |
23 +--------------------+-------------------------------------------------+
24 |elt* | A path that connects the subject and object |
25 | | of the path by zero or more matches of elt. |
26 +--------------------+-------------------------------------------------+
27 |elt+ | A path that connects the subject and object |
28 | | of the path by one or more matches of elt. |
29 +--------------------+-------------------------------------------------+
30 |elt? | A path that connects the subject and object |
31 | | of the path by zero or one matches of elt. |
32 +--------------------+-------------------------------------------------+
33 |!iri or | Negated property set. An IRI which is not one of|
34 |!(iri\ :sub:`1`\ | | iri\ :sub:`1`...iri\ :sub:`n`. |
35 |... |iri\ :sub:`n`) | !iri is short for !(iri). |
36 +--------------------+-------------------------------------------------+
37 |!^iri or | Negated property set where the excluded matches |
38 |!(^iri\ :sub:`1`\ | | are based on reversed path. That is, not one of |
39 |... |^iri\ :sub:`n`)| iri\ :sub:`1`...iri\ :sub:`n` as reverse paths. |
40 | | !^iri is short for !(^iri). |
41 +--------------------+-------------------------------------------------+
42 |!(iri\ :sub:`1`\ | | A combination of forward and reverse |
43 |...|iri\ :sub:`j`\ || properties in a negated property set. |
44 |^iri\ :sub:`j+1`\ | | |
45 |... |^iri\ :sub:`n`)| |
46 +--------------------+-------------------------------------------------+
47 |(elt) | A group path elt, brackets control precedence. |
48 +--------------------+-------------------------------------------------+
49
50 This module is used internally be the SPARQL engine, but they property paths
51 can also be used to query RDFLib Graphs directly.
52
53 Where possible the SPARQL syntax is mapped to python operators, and property
54 path objects can be constructed from existing URIRefs.
55
56 >>> from rdflib import Graph, Namespace
57
58 >>> foaf=Namespace('http://xmlns.com/foaf/0.1/')
59
60 >>> ~foaf.knows
61 Path(~http://xmlns.com/foaf/0.1/knows)
62
63 >>> foaf.knows/foaf.name
64 Path(http://xmlns.com/foaf/0.1/knows / http://xmlns.com/foaf/0.1/name)
65
66 >>> foaf.name|foaf.firstName
67 Path(http://xmlns.com/foaf/0.1/name | http://xmlns.com/foaf/0.1/firstName)
68
69 Modifiers (?, *, +) are done using * (the multiplication operator) and
70 the strings '*', '?', '+', also defined as constants in this file.
71
72 >>> foaf.knows*OneOrMore
73 Path(http://xmlns.com/foaf/0.1/knows+)
74
75 The path objects can also be used with the normal graph methods.
76
77 First some example data:
78
79 >>> g=Graph()
80
81 >>> g=g.parse(data='''
82 ... @prefix : <ex:> .
83 ...
84 ... :a :p1 :c ; :p2 :f .
85 ... :c :p2 :e ; :p3 :g .
86 ... :g :p3 :h ; :p2 :j .
87 ... :h :p3 :a ; :p2 :g .
88 ...
89 ... :q :px :q .
90 ...
91 ... ''', format='n3') # doctest: +ELLIPSIS
92
93 >>> e=Namespace('ex:')
94
95 Graph contains:
96 >>> (e.a, e.p1/e.p2, e.e) in g
97 True
98
99 Graph generator functions, triples, subjects, objects, etc. :
100
101 >>> list(g.objects(e.c, (e.p3*OneOrMore)/e.p2)) # doctest: +NORMALIZE_WHITESPACE
102 [rdflib.term.URIRef(%(u)s'ex:j'), rdflib.term.URIRef(%(u)s'ex:g'),
103 rdflib.term.URIRef(%(u)s'ex:f')]
104
105 A more complete set of tests:
106
107 >>> list(evalPath(g, (None, e.p1/e.p2, None)))==[(e.a, e.e)]
108 True
109 >>> list(evalPath(g, (e.a, e.p1|e.p2, None)))==[(e.a,e.c), (e.a,e.f)]
110 True
111 >>> list(evalPath(g, (e.c, ~e.p1, None))) == [ (e.c, e.a) ]
112 True
113 >>> list(evalPath(g, (e.a, e.p1*ZeroOrOne, None))) == [(e.a, e.a), (e.a, e.c)]
114 True
115 >>> list(evalPath(g, (e.c, e.p3*OneOrMore, None))) == [
116 ... (e.c, e.g), (e.c, e.h), (e.c, e.a)]
117 True
118 >>> list(evalPath(g, (e.c, e.p3*ZeroOrMore, None))) == [(e.c, e.c),
119 ... (e.c, e.g), (e.c, e.h), (e.c, e.a)]
120 True
121 >>> list(evalPath(g, (e.a, -e.p1, None))) == [(e.a, e.f)]
122 True
123 >>> list(evalPath(g, (e.a, -(e.p1|e.p2), None))) == []
124 True
125 >>> list(evalPath(g, (e.g, -~e.p2, None))) == [(e.g, e.j)]
126 True
127 >>> list(evalPath(g, (e.e, ~(e.p1/e.p2), None))) == [(e.e, e.a)]
128 True
129 >>> list(evalPath(g, (e.a, e.p1/e.p3/e.p3, None))) == [(e.a, e.h)]
130 True
131
132 >>> list(evalPath(g, (e.q, e.px*OneOrMore, None)))
133 [(rdflib.term.URIRef(%(u)s'ex:q'), rdflib.term.URIRef(%(u)s'ex:q'))]
134
135 >>> list(evalPath(g, (None, e.p1|e.p2, e.c)))
136 [(rdflib.term.URIRef(%(u)s'ex:a'), rdflib.term.URIRef(%(u)s'ex:c'))]
137
138 >>> list(evalPath(g, (None, ~e.p1, e.a))) == [ (e.c, e.a) ]
139 True
140 >>> list(evalPath(g, (None, e.p1*ZeroOrOne, e.c))) # doctest: +NORMALIZE_WHITESPACE
141 [(rdflib.term.URIRef(%(u)s'ex:c'), rdflib.term.URIRef(%(u)s'ex:c')),
142 (rdflib.term.URIRef(%(u)s'ex:a'), rdflib.term.URIRef(%(u)s'ex:c'))]
143
144 >>> list(evalPath(g, (None, e.p3*OneOrMore, e.a))) # doctest: +NORMALIZE_WHITESPACE
145 [(rdflib.term.URIRef(%(u)s'ex:h'), rdflib.term.URIRef(%(u)s'ex:a')),
146 (rdflib.term.URIRef(%(u)s'ex:g'), rdflib.term.URIRef(%(u)s'ex:a')),
147 (rdflib.term.URIRef(%(u)s'ex:c'), rdflib.term.URIRef(%(u)s'ex:a'))]
148
149 >>> list(evalPath(g, (None, e.p3*ZeroOrMore, e.a))) # doctest: +NORMALIZE_WHITESPACE
150 [(rdflib.term.URIRef(%(u)s'ex:a'), rdflib.term.URIRef(%(u)s'ex:a')),
151 (rdflib.term.URIRef(%(u)s'ex:h'), rdflib.term.URIRef(%(u)s'ex:a')),
152 (rdflib.term.URIRef(%(u)s'ex:g'), rdflib.term.URIRef(%(u)s'ex:a')),
153 (rdflib.term.URIRef(%(u)s'ex:c'), rdflib.term.URIRef(%(u)s'ex:a'))]
154
155 >>> list(evalPath(g, (None, -e.p1, e.f))) == [(e.a, e.f)]
156 True
157 >>> list(evalPath(g, (None, -(e.p1|e.p2), e.c))) == []
158 True
159 >>> list(evalPath(g, (None, -~e.p2, e.j))) == [(e.g, e.j)]
160 True
161 >>> list(evalPath(g, (None, ~(e.p1/e.p2), e.a))) == [(e.e, e.a)]
162 True
163 >>> list(evalPath(g, (None, e.p1/e.p3/e.p3, e.h))) == [(e.a, e.h)]
164 True
165
166 >>> list(evalPath(g, (e.q, e.px*OneOrMore, None)))
167 [(rdflib.term.URIRef(%(u)s'ex:q'), rdflib.term.URIRef(%(u)s'ex:q'))]
168
169 >>> list(evalPath(g, (e.c, (e.p2|e.p3)*ZeroOrMore, e.j)))
170 [(rdflib.term.URIRef(%(u)s'ex:c'), rdflib.term.URIRef(%(u)s'ex:j'))]
171
172 No vars specified:
173
174 >>> sorted(list(evalPath(g, (None, e.p3*OneOrMore, None)))) #doctest: +NORMALIZE_WHITESPACE
175 [(rdflib.term.URIRef(%(u)s'ex:c'), rdflib.term.URIRef(%(u)s'ex:a')),
176 (rdflib.term.URIRef(%(u)s'ex:c'), rdflib.term.URIRef(%(u)s'ex:g')),
177 (rdflib.term.URIRef(%(u)s'ex:c'), rdflib.term.URIRef(%(u)s'ex:h')),
178 (rdflib.term.URIRef(%(u)s'ex:g'), rdflib.term.URIRef(%(u)s'ex:a')),
179 (rdflib.term.URIRef(%(u)s'ex:g'), rdflib.term.URIRef(%(u)s'ex:h')),
180 (rdflib.term.URIRef(%(u)s'ex:h'), rdflib.term.URIRef(%(u)s'ex:a'))]
181
182 .. versionadded:: 4.0
183
184 """)
185
186
187 from rdflib.term import URIRef, Node
188
189
190 # property paths
191
192 ZeroOrMore = '*'
193 OneOrMore = '+'
194 ZeroOrOne = '?'
195
196
197 class Path(object):
198 def eval(self, graph, subj=None, obj=None):
199 raise NotImplementedError()
200
201 def __hash__(self):
202 return hash(repr(self))
203
204 def __eq__(self, other):
205 return repr(self) == repr(other)
206
207 def __lt__(self, other):
208 if not isinstance(other, (Path, Node)):
209 raise TypeError('unorderable types: %s() < %s()' % (
210 repr(self), repr(other)))
211 return repr(self) < repr(other)
212
213 def __le__(self, other):
214 if not isinstance(other, (Path, Node)):
215 raise TypeError('unorderable types: %s() < %s()' % (
216 repr(self), repr(other)))
217 return repr(self) <= repr(other)
218
219 def __ne__(self, other):
220 return not self == other
221
222 def __gt__(self, other):
223 return not self <= other
224
225 def __ge__(self, other):
226 return not self < other
227
228
229 class InvPath(Path):
230
231 def __init__(self, arg):
232 self.arg = arg
233
234 def eval(self, graph, subj=None, obj=None):
235 for s, o in evalPath(graph, (obj, self.arg, subj)):
236 yield o, s
237
238 def __repr__(self):
239 return "Path(~%s)" % (self.arg,)
240
241 def n3(self):
242 return '^%s' % self.arg.n3()
243
244
245 class SequencePath(Path):
246 def __init__(self, *args):
247 self.args = []
248 for a in args:
249 if isinstance(a, SequencePath):
250 self.args += a.args
251 else:
252 self.args.append(a)
253
254 def eval(self, graph, subj=None, obj=None):
255 def _eval_seq(paths, subj, obj):
256 if paths[1:]:
257 for s, o in evalPath(graph, (subj, paths[0], None)):
258 for r in _eval_seq(paths[1:], o, obj):
259 yield s, r[1]
260
261 else:
262 for s, o in evalPath(graph, (subj, paths[0], obj)):
263 yield s, o
264
265 def _eval_seq_bw(paths, subj, obj):
266 if paths[:-1]:
267 for s, o in evalPath(graph, (None, paths[-1], obj)):
268 for r in _eval_seq(paths[:-1], subj, s):
269 yield r[0], o
270
271 else:
272 for s, o in evalPath(graph, (subj, paths[0], obj)):
273 yield s, o
274
275 if subj:
276 return _eval_seq(self.args, subj, obj)
277 elif obj:
278 return _eval_seq_bw(self.args, subj, obj)
279 else: # no vars bound, we can start anywhere
280 return _eval_seq(self.args, subj, obj)
281
282 def __repr__(self):
283 return "Path(%s)" % " / ".join(str(x) for x in self.args)
284
285 def n3(self):
286 return '/'.join(a.n3() for a in self.args)
287
288
289 class AlternativePath(Path):
290 def __init__(self, *args):
291 self.args = []
292 for a in args:
293 if isinstance(a, AlternativePath):
294 self.args += a.args
295 else:
296 self.args.append(a)
297
298 def eval(self, graph, subj=None, obj=None):
299 for x in self.args:
300 for y in evalPath(graph, (subj, x, obj)):
301 yield y
302
303 def __repr__(self):
304 return "Path(%s)" % " | ".join(str(x) for x in self.args)
305
306 def n3(self):
307 return '|'.join(a.n3() for a in self.args)
308
309
310
311 class MulPath(Path):
312 def __init__(self, path, mod):
313 self.path = path
314 self.mod = mod
315
316 if mod == ZeroOrOne:
317 self.zero = True
318 self.more = False
319 elif mod == ZeroOrMore:
320 self.zero = True
321 self.more = True
322 elif mod == OneOrMore:
323 self.zero = False
324 self.more = True
325 else:
326 raise Exception('Unknown modifier %s' % mod)
327
328 def eval(self, graph, subj=None, obj=None, first=True):
329 if self.zero and first:
330 if subj and obj:
331 if subj == obj:
332 yield (subj, obj)
333 elif subj:
334 yield (subj, subj)
335 elif obj:
336 yield (obj, obj)
337
338 def _fwd(subj=None, obj=None, seen=None):
339 seen.add(subj)
340
341 for s, o in evalPath(graph, (subj, self.path, None)):
342 if not obj or o == obj:
343 yield s, o
344 if self.more:
345 if o in seen:
346 continue
347 for s2, o2 in _fwd(o, obj, seen):
348 yield s, o2
349
350 def _bwd(subj=None, obj=None, seen=None):
351 seen.add(obj)
352
353 for s, o in evalPath(graph, (None, self.path, obj)):
354 if not subj or subj == s:
355 yield s, o
356 if self.more:
357 if s in seen:
358 continue
359
360 for s2, o2 in _bwd(None, s, seen):
361 yield s2, o
362
363 def _fwdbwd():
364 if self.zero:
365 seen1 = set()
366 # According to the spec, ALL nodes are possible solutions
367 # (even literals)
368 # we cannot do this without going through ALL triples
369 # unless we keep an index of all terms somehow
370 # but lets just hope this query doesnt happen very often...
371 for s, o in graph.subject_objects(None):
372 if s not in seen1:
373 seen1.add(s)
374 yield s, s
375 if o not in seen1:
376 seen1.add(o)
377 yield o, o
378
379 for s, o in evalPath(graph, (None, self.path, None)):
380 if not self.more:
381 yield s, o
382 else:
383 seen = set()
384 f = list(_fwd(s, None, seen)) # cache or recompute?
385 for s3, o3 in _bwd(None, o, seen):
386 for s2, o2 in f:
387 yield s3, o2 # ?
388
389 done = set() # the spec does by defn. not allow duplicates
390 if subj:
391 for x in _fwd(subj, obj, set()):
392 if x not in done:
393 done.add(x)
394 yield x
395 elif obj:
396 for x in _bwd(subj, obj, set()):
397 if x not in done:
398 done.add(x)
399 yield x
400 else:
401 for x in _fwdbwd():
402 if x not in done:
403 done.add(x)
404 yield x
405
406 def __repr__(self):
407 return "Path(%s%s)" % (self.path, self.mod)
408
409 def n3(self):
410 return '%s%s' % (self.path, self.mod)
411
412
413
414 class NegatedPath(Path):
415 def __init__(self, arg):
416 if isinstance(arg, (URIRef, InvPath)):
417 self.args = [arg]
418 elif isinstance(arg, AlternativePath):
419 self.args = arg.args
420 else:
421 raise Exception(
422 'Can only negate URIRefs, InvPaths or ' +
423 'AlternativePaths, not: %s' % (arg,))
424
425 def eval(self, graph, subj=None, obj=None):
426 for s, p, o in graph.triples((subj, None, obj)):
427 for a in self.args:
428 if isinstance(a, URIRef):
429 if p == a:
430 break
431 elif isinstance(a, InvPath):
432 if (o, a.arg, s) in graph:
433 break
434 else:
435 raise Exception('Invalid path in NegatedPath: %s' % a)
436 else:
437 yield s, o
438
439 def __repr__(self):
440 return "Path(! %s)" % ",".join(str(x) for x in self.args)
441
442 def n3(self):
443 return '!(%s)' % ('|'.join(self.args))
444
445
446 class PathList(list):
447 pass
448
449
450 def path_alternative(self, other):
451 """
452 alternative path
453 """
454 if not isinstance(other, (URIRef, Path)):
455 raise Exception('Only URIRefs or Paths can be in paths!')
456 return AlternativePath(self, other)
457
458
459 def path_sequence(self, other):
460 """
461 sequence path
462 """
463 if not isinstance(other, (URIRef, Path)):
464 raise Exception('Only URIRefs or Paths can be in paths!')
465 return SequencePath(self, other)
466
467
468 def evalPath(graph, t):
469 return ((s, o) for s, p, o in graph.triples(t))
470
471 def mul_path(p, mul):
472 """
473 cardinality path
474 """
475 return MulPath(p, mul)
476
477
478 def inv_path(p):
479 """
480 inverse path
481 """
482 return InvPath(p)
483
484
485 def neg_path(p):
486 """
487 negated path
488 """
489 return NegatedPath(p)
490
491
492
493 if __name__ == '__main__':
494
495 import doctest
496 doctest.testmod()
497 else:
498 # monkey patch
499 # (these cannot be directly in terms.py
500 # as it would introduce circular imports)
501
502 URIRef.__or__ = path_alternative
503 URIRef.__mul__ = mul_path
504 URIRef.__invert__ = inv_path
505 URIRef.__neg__ = neg_path
506 URIRef.__truediv__ = path_sequence
507 if not PY3:
508 URIRef.__div__ = path_sequence
509
510 Path.__invert__ = inv_path
511 Path.__neg__ = neg_path
512 Path.__mul__ = mul_path
513 Path.__or__ = path_alternative
514 Path.__truediv__ = path_sequence
515 if not PY3:
516 Path.__div__ = path_sequence