9
9
10
10
from .catalog import SpecCatalog
11
11
from .spec import DatasetSpec , GroupSpec
12
- from ..utils import docval , getargs , popargs , get_docval
12
+ from ..utils import docval , getargs , popargs , get_docval , is_newer_version
13
13
14
14
_namespace_args = [
15
15
{'name' : 'doc' , 'type' : str , 'doc' : 'a description about what this namespace represents' },
@@ -229,13 +229,19 @@ class NamespaceCatalog:
229
229
{'name' : 'dataset_spec_cls' , 'type' : type ,
230
230
'doc' : 'the class to use for dataset specifications' , 'default' : DatasetSpec },
231
231
{'name' : 'spec_namespace_cls' , 'type' : type ,
232
- 'doc' : 'the class to use for specification namespaces' , 'default' : SpecNamespace })
232
+ 'doc' : 'the class to use for specification namespaces' , 'default' : SpecNamespace },
233
+ {'name' : 'core_namespaces' , 'type' : list ,
234
+ 'doc' : 'the names of the core namespaces' , 'default' : list ()})
233
235
def __init__ (self , ** kwargs ):
234
236
"""Create a catalog for storing multiple Namespaces"""
235
237
self .__namespaces = OrderedDict ()
236
238
self .__dataset_spec_cls = getargs ('dataset_spec_cls' , kwargs )
237
239
self .__group_spec_cls = getargs ('group_spec_cls' , kwargs )
238
240
self .__spec_namespace_cls = getargs ('spec_namespace_cls' , kwargs )
241
+
242
+ core_namespaces = getargs ('core_namespaces' , kwargs )
243
+ self .__core_namespaces = core_namespaces
244
+
239
245
# keep track of all spec objects ever loaded, so we don't have
240
246
# multiple object instances of a spec
241
247
self .__loaded_specs = dict ()
@@ -248,6 +254,7 @@ def __copy__(self):
248
254
ret = NamespaceCatalog (self .__group_spec_cls ,
249
255
self .__dataset_spec_cls ,
250
256
self .__spec_namespace_cls )
257
+ ret .__core_namespaces = copy (self .__core_namespaces )
251
258
ret .__namespaces = copy (self .__namespaces )
252
259
ret .__loaded_specs = copy (self .__loaded_specs )
253
260
ret .__included_specs = copy (self .__included_specs )
@@ -258,6 +265,8 @@ def merge(self, ns_catalog):
258
265
for name , namespace in ns_catalog .__namespaces .items ():
259
266
self .add_namespace (name , namespace )
260
267
268
+ self .__core_namespaces .extend (ns_catalog .__core_namespaces )
269
+
261
270
@property
262
271
@docval (returns = 'a tuple of the available namespaces' , rtype = tuple )
263
272
def namespaces (self ):
@@ -279,6 +288,11 @@ def spec_namespace_cls(self):
279
288
"""The SpecNamespace class used in this NamespaceCatalog"""
280
289
return self .__spec_namespace_cls
281
290
291
+ @property
292
+ def core_namespaces (self ):
293
+ """The core namespaces used in this NamespaceCatalog"""
294
+ return self .__core_namespaces
295
+
282
296
@docval ({'name' : 'name' , 'type' : str , 'doc' : 'the name of this namespace' },
283
297
{'name' : 'namespace' , 'type' : SpecNamespace , 'doc' : 'the SpecNamespace object' })
284
298
def add_namespace (self , ** kwargs ):
@@ -508,36 +522,122 @@ def __register_dependent_types_helper(spec, inc_ns, catalog, registered_types):
508
522
'type' : bool ,
509
523
'doc' : 'whether or not to include objects from included/parent spec objects' , 'default' : True },
510
524
{'name' : 'reader' ,
511
- 'type' : SpecReader ,
512
- 'doc' : 'the class to user for reading specifications' , 'default' : None },
525
+ 'type' : (SpecReader , dict ),
526
+ 'doc' : 'the SpecReader or dict of SpecReader classes to use for reading specifications' ,
527
+ 'default' : None },
513
528
returns = 'a dictionary describing the dependencies of loaded namespaces' , rtype = dict )
514
529
def load_namespaces (self , ** kwargs ):
515
530
"""Load the namespaces in the given file"""
516
531
namespace_path , resolve , reader = getargs ('namespace_path' , 'resolve' , 'reader' , kwargs )
532
+
533
+ # determine which readers and order of readers to use for loading specs
517
534
if reader is None :
518
535
# load namespace definition from file
519
536
if not os .path .exists (namespace_path ):
520
537
msg = "namespace file '%s' not found" % namespace_path
521
538
raise IOError (msg )
522
- reader = YAMLSpecReader (indir = os .path .dirname (namespace_path ))
523
- ns_path_key = os .path .join (reader .source , os .path .basename (namespace_path ))
524
- ret = self .__included_specs .get (ns_path_key )
525
- if ret is None :
526
- ret = dict ()
539
+ ordered_readers = [YAMLSpecReader (indir = os .path .dirname (namespace_path ))]
540
+ elif isinstance (reader , SpecReader ):
541
+ ordered_readers = [reader ] # only one reader
527
542
else :
528
- return ret
529
- namespaces = reader .read_namespace (namespace_path )
530
- to_load = list ()
531
- for ns in namespaces :
532
- if ns ['name' ] in self .__namespaces :
533
- if ns ['version' ] != self .__namespaces .get (ns ['name' ])['version' ]:
534
- # warn if the cached namespace differs from the already loaded namespace
535
- warn ("Ignoring cached namespace '%s' version %s because version %s is already loaded."
536
- % (ns ['name' ], ns ['version' ], self .__namespaces .get (ns ['name' ])['version' ]))
537
- else :
538
- to_load .append (ns )
539
- # now load specs into namespace
540
- for ns in to_load :
541
- ret [ns ['name' ]] = self .__load_namespace (ns , reader , resolve = resolve )
542
- self .__included_specs [ns_path_key ] = ret
543
+ deps = dict () # for each namespace, track all included namespaces (dependencies)
544
+ for ns , r in reader .items ():
545
+ for spec_ns in r .read_namespace (namespace_path ):
546
+ deps [ns ] = list ()
547
+ for s in spec_ns ['schema' ]:
548
+ dep = s .get ('namespace' )
549
+ if dep is not None :
550
+ deps [ns ].append (dep )
551
+ order = self ._order_deps (deps )
552
+ ordered_readers = [reader [ns ] for ns in order ]
553
+
554
+ # determine which namespaces to load and which to ignore
555
+ ignored_namespaces = list ()
556
+ ret = dict ()
557
+ for r in ordered_readers :
558
+ # continue to next reader if spec is already included
559
+ ns_path_key = os .path .join (r .source , os .path .basename (namespace_path ))
560
+ included_specs = self .__included_specs .get (ns_path_key )
561
+ if included_specs is not None :
562
+ ret .update (included_specs )
563
+ continue # continue to next reader if spec is already included
564
+
565
+ to_load = list ()
566
+ namespaces = r .read_namespace (namespace_path )
567
+ for ns in namespaces :
568
+ if ns ['name' ] in self .__namespaces :
569
+ if ns ['version' ] != self .__namespaces .get (ns ['name' ])['version' ]:
570
+ cached_version = ns ['version' ]
571
+ loaded_version = self .__namespaces .get (ns ['name' ])['version' ]
572
+ ignored_namespaces .append ((ns ['name' ], cached_version , loaded_version ))
573
+ else :
574
+ to_load .append (ns )
575
+
576
+ # now load specs into namespace
577
+ for ns in to_load :
578
+ ret [ns ['name' ]] = self .__load_namespace (ns , r , resolve = resolve )
579
+ self .__included_specs [ns_path_key ] = ret
580
+
581
+ # warn if there are any ignored namespaces
582
+ if ignored_namespaces :
583
+ self .warn_for_ignored_namespaces (ignored_namespaces )
584
+
543
585
return ret
586
+
587
+ def warn_for_ignored_namespaces (self , ignored_namespaces ):
588
+ """Warning if namespaces were ignored where a different version was already loaded
589
+
590
+ Args:
591
+ ignored_namespaces (list): name, cached version, and loaded version of the namespace
592
+ """
593
+ core_warnings = list ()
594
+ other_warnings = list ()
595
+ warning_msg = list ()
596
+ for name , cached_version , loaded_version in ignored_namespaces :
597
+ version_info = f"{ name } - cached version: { cached_version } , loaded version: { loaded_version } "
598
+ if name in self .__core_namespaces and is_newer_version (cached_version , loaded_version ):
599
+ core_warnings .append (version_info ) # for core namespaces, warn if the cached version is newer
600
+ elif name not in self .__core_namespaces :
601
+ other_warnings .append (version_info ) # for all other namespaces, issue a warning for compatibility
602
+
603
+ if core_warnings :
604
+ joined_warnings = "\n " .join (core_warnings )
605
+ warning_msg .append (f'{ joined_warnings } \n Please update to the latest package versions.' )
606
+ if other_warnings :
607
+ joined_warnings = "\n " .join (other_warnings )
608
+ warning_msg .append (f'{ joined_warnings } \n The loaded extension(s) may not be compatible with the cached '
609
+ 'extension(s) in the file. Please check the extension documentation and ignore this '
610
+ 'warning if these versions are compatible.' )
611
+ if warning_msg :
612
+ joined_warnings = "\n " .join (warning_msg )
613
+ warn (f'Ignoring the following cached namespace(s) because another version is already loaded:\n '
614
+ f'{ joined_warnings } ' , category = UserWarning , stacklevel = 2 )
615
+
616
+ def _order_deps (self , deps ):
617
+ """
618
+ Order namespaces according to dependency for loading into a NamespaceCatalog
619
+
620
+ Args:
621
+ deps (dict): a dictionary that maps a namespace name to a list of name of
622
+ the namespaces on which the namespace is directly dependent
623
+ Example: {'a': ['b', 'c'], 'b': ['d'], 'c': ['d'], 'd': []}
624
+ Expected output: ['d', 'b', 'c', 'a']
625
+ """
626
+ order = list ()
627
+ keys = list (deps .keys ())
628
+ deps = dict (deps )
629
+ for k in keys :
630
+ if k in deps :
631
+ self .__order_deps_aux (order , deps , k )
632
+ return order
633
+
634
+ def __order_deps_aux (self , order , deps , key ):
635
+ """
636
+ A recursive helper function for _order_deps
637
+ """
638
+ if key not in deps :
639
+ return
640
+ subdeps = deps .pop (key )
641
+ for subk in subdeps :
642
+ self .__order_deps_aux (order , deps , subk )
643
+ order .append (key )
0 commit comments