|
4 | 4 | import collections |
5 | 5 | from functools import lru_cache |
6 | 6 | from copy import copy, deepcopy |
7 | | -from collections import defaultdict |
| 7 | +from collections import defaultdict, deque |
8 | 8 | from pathlib import Path |
9 | 9 | from typing import Mapping, Tuple |
| 10 | +import warnings |
10 | 11 |
|
11 | 12 | from linkml_runtime.utils.namespaces import Namespaces |
12 | 13 | from deprecated.classic import deprecated |
@@ -207,34 +208,76 @@ def load_import(self, imp: str, from_schema: SchemaDefinition = None): |
207 | 208 | return schema |
208 | 209 |
|
209 | 210 | @lru_cache() |
210 | | - def imports_closure(self, imports: bool = True, traverse=True, inject_metadata=True) -> List[SchemaDefinitionName]: |
| 211 | + def imports_closure(self, imports: bool = True, traverse: Optional[bool] = None, inject_metadata=True) -> List[SchemaDefinitionName]: |
211 | 212 | """ |
212 | 213 | Return all imports |
213 | 214 |
|
214 | | - :param traverse: if true, traverse recursively |
| 215 | + Objects in imported classes override one another in a "python-like" order - |
| 216 | + from the point of view of the importing schema, imports will override one |
| 217 | + another from first to last, recursively for each layer of imports. |
| 218 | +
|
| 219 | + An import tree like:: |
| 220 | +
|
| 221 | + - main |
| 222 | + - s1 |
| 223 | + - s1_1 |
| 224 | + - s1_2 |
| 225 | + - s1_2_1 |
| 226 | + - s1_2_2 |
| 227 | + - s2 |
| 228 | + - s2_1 |
| 229 | + - s2_2 |
| 230 | +
|
| 231 | + will override objects with the same name, in order:: |
| 232 | +
|
| 233 | + ['s1_1', 's1_2_1', 's1_2_2', 's1_2', 's1', 's2_1', 's2_2', 's2'] |
| 234 | +
|
| 235 | + :param imports: bool (default: ``True`` ) include imported schemas, recursively |
| 236 | + :param traverse: bool, optional (default: ``True`` ) (Deprecated, use |
| 237 | + ``imports`` ). if true, traverse recursively |
215 | 238 | :return: all schema names in the transitive reflexive imports closure |
216 | 239 | """ |
217 | | - if not imports: |
218 | | - return [self.schema.name] |
219 | 240 | if self.schema_map is None: |
220 | 241 | self.schema_map = {self.schema.name: self.schema} |
221 | | - closure = [] |
| 242 | + |
| 243 | + closure = deque() |
222 | 244 | visited = set() |
223 | 245 | todo = [self.schema.name] |
224 | | - if not traverse: |
| 246 | + |
| 247 | + if traverse is not None: |
| 248 | + warnings.warn( |
| 249 | + 'traverse behaves identically to imports and will be removed in a future version. Use imports instead.', |
| 250 | + DeprecationWarning |
| 251 | + ) |
| 252 | + |
| 253 | + if not imports or (not traverse and traverse is not None): |
225 | 254 | return todo |
| 255 | + |
226 | 256 | while len(todo) > 0: |
| 257 | + # visit item |
227 | 258 | sn = todo.pop() |
228 | | - visited.add(sn) |
229 | 259 | if sn not in self.schema_map: |
230 | 260 | imported_schema = self.load_import(sn) |
231 | 261 | self.schema_map[sn] = imported_schema |
232 | | - s = self.schema_map[sn] |
233 | | - if sn not in closure: |
234 | | - closure.append(sn) |
235 | | - for i in s.imports: |
236 | | - if i not in visited: |
237 | | - todo.append(i) |
| 262 | + |
| 263 | + # resolve item's imports if it has not been visited already |
| 264 | + # we will get duplicates, but not cycles this way, and |
| 265 | + # filter out dupes, preserving the first entry, at the end. |
| 266 | + if sn not in visited: |
| 267 | + for i in self.schema_map[sn].imports: |
| 268 | + # no self imports ;) |
| 269 | + if i != sn: |
| 270 | + todo.append(i) |
| 271 | + |
| 272 | + # add item to closure |
| 273 | + # append + pop (above) is FILO queue, which correctly extends tree leaves, |
| 274 | + # but in backwards order. |
| 275 | + closure.appendleft(sn) |
| 276 | + visited.add(sn) |
| 277 | + |
| 278 | + # filter duplicates, keeping first entry |
| 279 | + closure = list({k:None for k in closure}.keys()) |
| 280 | + |
238 | 281 | if inject_metadata: |
239 | 282 | for s in self.schema_map.values(): |
240 | 283 | for x in {**s.classes, **s.enums, **s.slots, **s.subsets, **s.types}.values(): |
@@ -434,6 +477,7 @@ def all_elements(self, imports=True) -> Dict[ElementName, Element]: |
434 | 477 | def _get_dict(self, slot_name: str, imports=True) -> Dict: |
435 | 478 | schemas = self.all_schema(imports) |
436 | 479 | d = {} |
| 480 | + # pdb.set_trace() |
437 | 481 | # iterate through all schemas and merge the list together |
438 | 482 | for s in schemas: |
439 | 483 | # get the value of element name from the schema, if empty, return empty dictionary. |
|
0 commit comments