-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathutils.py
More file actions
419 lines (327 loc) · 13.8 KB
/
Copy pathutils.py
File metadata and controls
419 lines (327 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
"""Utility resources for the ontology module."""
from __future__ import annotations
import importlib
import os
import pkgutil
from abc import ABC, abstractmethod
from functools import lru_cache, reduce
from typing import Any, Dict, Iterable, Iterator, MutableSet, Set, Tuple
from simphony_osp.ontology.entity import OntologyEntity
class DataStructureSet(ABC, MutableSet):
"""Set-like object that acts as an interface to another data structure.
This class looks like and acts like the standard `set`, but the data it
holds is stored on a different data structure. When an instance is read
or when it is modified in-place, specific methods defined to interact
with the data structure are called to reflect the changes.
This class does not hold any object-related information itself, thus
it is safe to spawn multiple instances linked to the same data
structure (when single-threading).
"""
@abstractmethod
def __iter__(self) -> Iterator[Any]:
"""Implement iter(self).
Returns the associated values from the connected data structure.
The values provided MUST NOT be repeated, as instances of this class
are expected to behave like sets. Remember that the definition of
membership for sets in Python is `any(x is e or x == e for e in y)`,
see https://docs.python.org/3/reference/expressions.html
#membership-test-details. Please make sure this function returns
unique items according to the definition above.
Returns:
An iterator providing the values from the connected data
structure.
"""
pass
@abstractmethod
def __contains__(self, item: Any) -> bool:
"""Return y in x.
Check if the item is in the connected data structure.
You can also choose to just call `return super().__contains__(item)` to
use this naive implementation instead of implementing something
yourself.
"""
for x in self:
if x is item or x == item:
return True
return False
@abstractmethod
def update(self, other: Iterable) -> None:
"""Update a set with the union of itself and others.
Updates the connected data structure with the result of performing a
set union operation with another iterable.
"""
pass
@abstractmethod
def intersection_update(self, other: Iterable) -> None:
"""Update a set with the intersection of itself and another.
Updates the connected data structure performing a set intersection
operation with another iterable.
"""
pass
@abstractmethod
def difference_update(self, other: Iterable) -> None:
"""Remove all elements of another set from this set.
Updates the connected data structure performing a set difference
operation with another iterable.
"""
pass
@abstractmethod
def symmetric_difference_update(self, other: Iterable) -> None:
"""Update a set with the symmetric difference of itself and another.
Updates the connected data structure performing an XOR set operation
with another iterable.
"""
pass
def __repr__(self) -> str:
"""Return repr(self)."""
return set(self).__repr__()
def __str__(self) -> str:
"""Return str(self)."""
return set(self).__str__()
def __format__(self, format_spec: str) -> str:
"""Default object formatter."""
return set(self).__format__(format_spec)
def __len__(self) -> int:
"""Return len(self)."""
return sum(1 for _ in self)
def __le__(self, other: set) -> bool:
"""Return self<=other."""
return set(self).__le__(other)
def __lt__(self, other: set) -> bool:
"""Return self<other."""
return set(self).__lt__(other)
def __eq__(self, other: set) -> bool:
"""Return self==other."""
return set(self).__eq__(other)
def __ne__(self, other: set) -> bool:
"""Return self!=other."""
return set(self).__ne__(other)
def __gt__(self, other: set) -> bool:
"""Return self>other."""
return set(self).__gt__(other)
def __ge__(self, other: set) -> bool:
"""Return self>=other."""
return set(self).__ge__(other)
def __and__(self, other: set) -> set:
"""Return self&other."""
return set(self).__and__(other)
def __radd__(self, other: set) -> set:
"""Return other&self."""
return other & set(self)
def __ror__(self, other: set) -> set:
"""Return other|self."""
return other | set(self)
def __rsub__(self, other: set) -> set:
"""Return value-self."""
return other - set(self)
def __rxor__(self, other: set) -> set:
"""Return value^self."""
return other ^ set(self)
def __or__(self, other: set) -> set:
"""Return self|other."""
return set(self).__or__(other)
def __sub__(self, other: set) -> set:
"""Return self-other."""
return set(self).__sub__(other)
def __xor__(self, other: set) -> set:
"""Return self^other."""
return set(self).__xor__(other)
def __iadd__(self, other: Any) -> DataStructureSet:
"""Return self+=other (equivalent to self|=other)."""
if isinstance(other, (Tuple, Set, MutableSet)):
# Apparently instances of MutableSet are not instances of Set.
self.update(other)
else:
self.update({other})
return self
def __isub__(self, other: Any) -> DataStructureSet:
"""Return self-=other.
Based on `difference_update`.
"""
if isinstance(other, (Tuple, Set, MutableSet)):
# Apparently instances of MutableSet are not instances of Set.
self.difference_update(other)
else:
self.difference_update({other})
return self
def __ior__(self, other: set) -> DataStructureSet:
"""Return self|=other.
Should perform the union on the underlying data structure.
"""
self.update(other)
return self
def __iand__(self, other: set) -> DataStructureSet:
"""Return self&=other.
Should perform the intersection on the underlying data structure.
"""
self.intersection_update(other)
return self
def __ixor__(self, other: set) -> DataStructureSet:
"""Return self^=other.
Should perform the XOR operation on the underlying data structure.
"""
self.symmetric_difference_update(other)
return self
def isdisjoint(self, other: set) -> bool:
"""Return True if two sets have a null intersection."""
return set(self).isdisjoint(other)
def clear(self) -> None:
"""Remove all elements from this set."""
self.intersection_update(set())
def pop(self) -> Any:
"""Remove and return an arbitrary set element.
Raises KeyError if the set is empty.
"""
try:
item = next(iter(self))
except StopIteration:
return set().pop() # Underlying data structure is empty.
# The `KeyError` is raised like this intentionally, to avoid
# hardcoding the error text for a set.
self.difference_update({item})
return item
def copy(self) -> set:
"""Return a shallow copy of a set."""
return set(self)
def difference(self, other: Iterable) -> set:
"""Return the difference of two or more sets as a new set.
(i.e. all elements that are in this set but not the others.)
"""
return set(self).difference(other)
def discard(self, other: Any) -> None:
"""Remove an element from a set if it is a member.
If the element is not a member, do nothing.
"""
self.difference_update({other})
def intersection(self, other: set) -> set:
"""Return the intersection of two sets as a new set.
(i.e. all elements that are in both sets.)
"""
return set(self).intersection(other)
def issubset(self, other: set) -> bool:
"""Report whether another set contains this set."""
return self <= other
def issuperset(self, other: set) -> bool:
"""Report whether this set contains another set."""
return self >= other
def add(self, other: Any) -> None:
"""Add an element to a set.
This has no effect if the element is already present.
"""
self.update({other})
def remove(self, other: Any) -> None:
"""Remove an element from a set; it must be a member.
If the element is not a member, raise a KeyError.
"""
if other not in self:
raise KeyError(f"{other}")
self.difference_update({other})
def symmetric_difference(self, other: set) -> set:
"""Return the symmetric difference of two sets as a new set."""
return set(self).symmetric_difference(other)
def union(self, other: set) -> set:
"""Return the union of sets as a new set."""
return set(self).union(other)
"""Define a `compatible_classes` function that lists the Python classes that
can be spawned for a particular RDF.type and type of node identifier.
This function used by `simphony_osp.session.session.Session.from_identifier` to
determine which Python class should be spawned for a given IRI or BNode.
Read the docstrings of the functions defined in this section for more details.
"""
# ↓ --------------- ↓
@lru_cache(maxsize=None)
def _compute_mappings() -> Tuple[Dict[Any, Any], Dict[Any, Any]]:
"""Maps RDF types and node identifier types to Python classes.
The classes defined in SimPhoNy that are meant to represent ontology
entities (all subclasses of `OntologyEntity`), have two attributes,
`rdf_type` and `rdf_identifier` that determine the combination of RDF
types (e.g. owl:Class) and node identifier types (e.g. URIRef, BNode) that
the class is meant to represent.
This function imports the `simphony_osp.ontology` module and all of
its submodules recursively, as all subclasses of `OntologyEntity` are
expected to be stored there.
After that, it generates two mappings, `mapping_rdf_to_python_class` and
`mapping_identifier_to_python_class`, that map each rdf type and node
identifier type to compatible Python classes.
Using such mappings, other functions can find out the compatible classes
for a specific pair of RDF type and node identifier type.
This function is cached, as it is called by `compatible_classes`
repeatedly, but the computation is only needed once. In fact, this code
could be defined outside a function, but it has been incorporated
into a function because of the need to evaluate it lazily (to avoid
circular imports).
Returns:
A tuple `mapping_rdf_to_python_class,
mapping_identifier_to_python_class` containing the aforementioned
mappings.
"""
# First, import the ontology module and all of its submodules recursively.
self = __import__(__name__)
package_paths = [
os.path.abspath(os.path.join(path, "ontology"))
for path in self.__path__
]
package_prefix = f"{self.__name__}.ontology."
def import_modules_recursively(paths, prefix):
for module_info in pkgutil.iter_modules(paths, prefix):
module = importlib.import_module(module_info.name)
if module_info.ispkg:
import_modules_recursively(
module.__path__, f"{module_info.name}."
)
import_modules_recursively(package_paths, package_prefix)
# Then compute the mappings, remember that the python class to instantiate
# for a given ontology entity depends on two things:
# - The RDF.type(s) of the identifier.
# - The RDF node type of the identifier (URIRef, Node or Literal)
mapping_rdf_to_python_class = dict()
mapping_identifier_to_python_class = dict()
def recursive_iterator(class_):
for sub_class in class_.__subclasses__():
yield from recursive_iterator(sub_class)
yield sub_class
for subclass in recursive_iterator(OntologyEntity):
rdf_types = (
subclass.rdf_type
if isinstance(subclass.rdf_type, set)
else {subclass.rdf_type}
)
for rdf_type in rdf_types:
mapping_rdf_to_python_class[rdf_type] = (
mapping_rdf_to_python_class.get(rdf_type, set()) | {subclass}
)
rdf_identifiers = (
subclass.rdf_identifier
if isinstance(subclass.rdf_identifier, set)
else {subclass.rdf_identifier}
)
for rdf_identifier in rdf_identifiers:
mapping_identifier_to_python_class[rdf_identifier] = (
mapping_identifier_to_python_class.get(rdf_identifier, set())
| {subclass}
)
return mapping_rdf_to_python_class, mapping_identifier_to_python_class
@lru_cache(maxsize=4096)
def compatible_classes(type_, identifier):
"""Given a pair of an RDF type and an identifier get a Python class.
Given a pair of an RDF type and an identifier, the compatible Python
classes are computed. In fact, for the latter only the type of
identifier matters.
"""
(
mapping_rdf_to_python_class,
mapping_identifier_to_python_class,
) = _compute_mappings()
# Remember that the call above is cached (see `_compute_mappings`).
from_type = mapping_rdf_to_python_class.get(type_, set())
from_identifier = reduce(
lambda x, y: x | y,
(
value
for key, value in mapping_identifier_to_python_class.items()
if isinstance(identifier, key)
),
set(),
)
return from_type & from_identifier
# ↑ --------------- ↑