88import uuid
99import warnings
1010from collections import defaultdict , deque
11+ from collections .abc import Iterable
1112from copy import copy , deepcopy
1213from dataclasses import dataclass
1314from enum import Enum
4445from linkml_runtime .utils .pattern import PatternResolver
4546
4647if TYPE_CHECKING :
47- from collections .abc import Callable , Iterable , Mapping
48+ from collections .abc import Callable , Mapping
4849 from types import NotImplementedType
4950
5051 from linkml_runtime .utils .metamodelcore import URI , URIorCURIE
@@ -96,8 +97,13 @@ class OrderedBy(Enum):
9697BLACK = 2
9798
9899
99- def detect_cycles (f : Callable [[Any ], Iterable [Any ] | None ], x : Any ) -> None :
100- """Detect cycles in a graph, using function `f` to walk the graph, starting at node `x`.
100+ def detect_cycles (
101+ f : Callable [[Any ], Iterable [Any ] | None ],
102+ node_list : Iterable [Any ],
103+ ) -> None :
104+ """Detect cycles in a graph, using function `f` to walk the graph.
105+
106+ Input is supplied as a list of nodes that are used to populate the `todo` stack.
101107
102108 Uses the classic white/grey/black colour coding algorithm to track which nodes have been explored. In this
103109 case, "node" refers to any element in a schema and "neighbours" are elements that can be reached from that
@@ -107,21 +113,28 @@ def detect_cycles(f: Callable[[Any], Iterable[Any] | None], x: Any) -> None:
107113 GREY: node is being processed; processing includes exploring all neighbours reachable via f(node)
108114 BLACK: node and all of its neighbours (and their neighbours, etc.) have been processed
109115
110- A directed cycle reachable from node `x` raises a ValueError.
116+ A directed cycle reachable from a node or its neighbours raises a ValueError.
111117
112118 :param f: function that returns an iterable of neighbouring nodes (parents or children)
113119 :type f: Callable[[Any], Iterable[Any] | None]
114- :param x: graph node
115- :type x: Any
116- :raises ValueError: if a cycle is discovered through repeated calls to f(x )
120+ :param node_list: list or other iterable of values to process
121+ :type node_list: Iterable[ Any]
122+ :raises ValueError: if a cycle is discovered through repeated calls to f(node )
117123 """
124+ # ensure we have some nodes to start the analysis
125+ if not node_list or not isinstance (node_list , Iterable ) or isinstance (node_list , str ):
126+ err_msg = "detect_cycles requires a list of values to process"
127+ raise ValueError (err_msg )
128+
118129 # keep track of the processing state of nodes in the graph
119130 processing_state : dict [Any , int ] = {}
120131
121132 # Stack entries are (node, processed_flag).
122133 # processed_flag == True means all neighbours (nodes generated by running `f(node)`)
123134 # have been added to the todo stack and the node can be marked BLACK.
124- todo : list [tuple [Any , bool ]] = [(x , False )]
135+
136+ # initialise the todo stack with entries set to False
137+ todo : list [tuple [Any , bool ]] = [(node , False ) for node in node_list ]
125138
126139 while todo :
127140 node , processed_flag = todo .pop ()
@@ -173,7 +186,7 @@ def _closure(
173186 :rtype: list[str | ElementName | ClassDefinitionName | EnumDefinitionName | SlotDefinitionName | TypeDefinitionName]
174187 """
175188 if kwargs and kwargs .get ("detect_cycles" ):
176- detect_cycles (f , x )
189+ detect_cycles (f , [ x ] )
177190
178191 rv = [x ] if reflexive else []
179192 visited = []
@@ -452,8 +465,7 @@ def imports_closure(
452465 # visit item
453466 sn = todo .pop ()
454467 if sn not in self .schema_map :
455- imported_schema = self .load_import (sn )
456- self .schema_map [sn ] = imported_schema
468+ self .schema_map [sn ] = self .load_import (sn )
457469
458470 # resolve item's imports if it has not been visited already
459471 # we will get duplicates, but not cycles this way, and
@@ -529,13 +541,15 @@ def all_schema(self, imports: bool = True) -> list[SchemaDefinition]:
529541 def namespaces (self ) -> Namespaces :
530542 """Return the namespaces present in a schema.
531543
544+ Note: the output of this function will differ, depending on whether any functions that process imports have been run.
545+
532546 :return: namespaces
533547 :rtype: Namespaces
534548 """
535549 namespaces = Namespaces ()
550+ for cmap in self .schema .default_curi_maps :
551+ namespaces .add_prefixmap (cmap , include_defaults = False )
536552 for s in self .schema_map .values ():
537- for cmap in self .schema .default_curi_maps :
538- namespaces .add_prefixmap (cmap , include_defaults = False )
539553 for prefix in s .prefixes .values ():
540554 namespaces [prefix .prefix_prefix ] = prefix .prefix_reference
541555 return namespaces
@@ -884,7 +898,9 @@ def _parents(self, e: Element, imports: bool = True, mixins: bool = True, is_a:
884898 def class_parents (
885899 self , class_name : CLASS_NAME , imports : bool = True , mixins : bool = True , is_a : bool = True
886900 ) -> list [ClassDefinitionName ]:
887- """:param class_name: child class name
901+ """Get the parents of a class.
902+
903+ :param class_name: child class name
888904 :param imports: include import closure
889905 :param mixins: include mixins (default is True)
890906 :return: all direct parent class names (is_a and mixins)
@@ -1137,11 +1153,24 @@ def type_ancestors(
11371153 ** kwargs ,
11381154 )
11391155
1156+ @lru_cache (None )
1157+ def type_roots (self , imports : bool = True ) -> list [TypeDefinitionName ]:
1158+ """Return all types that have no parents.
1159+
1160+ :param imports: whether or not to include imports, defaults to True
1161+ :type imports: bool, optional
1162+ :return: list of all root types
1163+ :rtype: list[TypeDefinitionName]
1164+ """
1165+ return [t for t in self .all_types (imports = imports ) if not self .type_parents (t , imports = imports )]
1166+
11401167 @lru_cache (None )
11411168 def enum_parents (
11421169 self , enum_name : ENUM_NAME , imports : bool = False , mixins : bool = False , is_a : bool = True
11431170 ) -> list [EnumDefinitionName ]:
1144- """:param enum_name: child enum name
1171+ """Get the parents of an enum.
1172+
1173+ :param enum_name: child enum name
11451174 :param imports: include import closure (False)
11461175 :param mixins: include mixins (default is False)
11471176 :return: all direct parent enum names (is_a and mixins)
@@ -1178,12 +1207,20 @@ def enum_ancestors(
11781207 ** kwargs ,
11791208 )
11801209
1181- @lru_cache ( None )
1210+ @deprecated ( "Use `permissible_value_parents` instead" )
11821211 def permissible_value_parent (
11831212 self , permissible_value : str , enum_name : ENUM_NAME
11841213 ) -> list [str | PermissibleValueText ]:
1185- """:param enum_name: child enum name
1214+ return self .permissible_value_parents (permissible_value , enum_name )
1215+
1216+ @lru_cache (None )
1217+ def permissible_value_parents (
1218+ self , permissible_value : str , enum_name : ENUM_NAME
1219+ ) -> list [str | PermissibleValueText ]:
1220+ """Get the parents of a permissible value.
1221+
11861222 :param permissible_value: permissible value
1223+ :param enum_name: enum for which this is a permissible value
11871224 :return: all direct parent enum names (is_a)
11881225 """
11891226 enum = self .get_enum (enum_name , strict = True )
@@ -1197,8 +1234,10 @@ def permissible_value_parent(
11971234 def permissible_value_children (
11981235 self , permissible_value : str , enum_name : ENUM_NAME
11991236 ) -> list [str | PermissibleValueText ]:
1200- """:param enum_name: parent enum name
1237+ """Get the children of a permissible value.
1238+
12011239 :param permissible_value: permissible value
1240+ :param enum_name: enum for which this is a permissible value
12021241 :return: all direct child permissible values (is_a)
12031242 """
12041243 enum = self .get_enum (enum_name , strict = True )
@@ -1236,7 +1275,7 @@ def permissible_value_ancestors(
12361275 :rtype: list[str]
12371276 """
12381277 return _closure (
1239- lambda x : self .permissible_value_parent (x , enum_name ),
1278+ lambda x : self .permissible_value_parents (x , enum_name ),
12401279 permissible_value_text ,
12411280 reflexive = reflexive ,
12421281 depth_first = depth_first ,
@@ -1264,17 +1303,6 @@ def permissible_value_descendants(
12641303 ** kwargs ,
12651304 )
12661305
1267- @lru_cache (None )
1268- def type_roots (self , imports : bool = True ) -> list [TypeDefinitionName ]:
1269- """Return all types that have no parents.
1270-
1271- :param imports: whether or not to include imports, defaults to True
1272- :type imports: bool, optional
1273- :return: list of all root types
1274- :rtype: list[TypeDefinitionName]
1275- """
1276- return [t for t in self .all_types (imports = imports ) if not self .type_parents (t , imports = imports )]
1277-
12781306 @lru_cache (None )
12791307 def is_multivalued (self , slot_name : SlotDefinition ) -> bool :
12801308 """Return True if slot is multivalued, else returns False.
@@ -1429,13 +1457,12 @@ def get_elements_applicable_by_prefix(self, prefix: str) -> list[str]:
14291457 :return: Optional[str]
14301458
14311459 """
1432- applicable_elements = []
14331460 elements = self .all_elements ()
1434- for category_element in elements . values ():
1435- if hasattr ( category_element , "id_prefixes" ) and prefix in category_element . id_prefixes :
1436- applicable_elements . append ( category_element . name )
1437-
1438- return applicable_elements
1461+ return [
1462+ element . name
1463+ for element in elements . values ( )
1464+ if hasattr ( element , "id_prefixes" ) and prefix in element . id_prefixes
1465+ ]
14391466
14401467 @lru_cache (None )
14411468 def all_aliases (self ) -> list [str ]:
@@ -1817,6 +1844,33 @@ def get_type_designator_slot(self, cn: CLASS_NAME, imports: bool = True) -> Slot
18171844 return s
18181845 return None
18191846
1847+ @lru_cache (None )
1848+ def _get_string_type (self ) -> TypeDefinition :
1849+ """Get the type used for representing strings.
1850+
1851+ This can be used for (e.g.) retrieving the appropriate type for a slot where the range is an enum.
1852+
1853+ The method assumes that the string type will either be called "string" or
1854+ will have the URI "xsd:string", as is the case for the "string" type in linkml:types.
1855+
1856+ This method throws an error if there is anything other than one type that fits the criteria.
1857+
1858+ :return: the "string" type object
1859+ :rtype: TypeDefinition
1860+ """
1861+ str_type = self .get_type ("string" )
1862+ if str_type :
1863+ return str_type
1864+
1865+ # if there isn't a type named "string", search for a type with the URI xsd:string
1866+ str_type_arr = [v for v in self .all_types ().values () if v .uri == "xsd:string" ]
1867+ if len (str_type_arr ) == 1 :
1868+ return str_type_arr [0 ]
1869+
1870+ # zero or more than one potential "string" type found
1871+ err_msg = f"Cannot find a suitable 'string' type: no types with name 'string' { 'and more than one type with' if str_type_arr else 'or' } uri 'xsd:string'."
1872+ raise ValueError (err_msg )
1873+
18201874 def is_inlined (self , slot : SlotDefinition , imports : bool = True ) -> bool :
18211875 """Return true if slot is inferred or asserted inline.
18221876
@@ -1849,6 +1903,10 @@ def slot_applicable_range_elements(self, slot: SlotDefinition) -> list[ClassDefi
18491903 :param slot:
18501904 :return: list of element types
18511905 """
1906+ if not slot or not isinstance (slot , SlotDefinition ):
1907+ err_msg = "A SlotDefinition must be provided to generate the slot applicable range elements."
1908+ raise ValueError (err_msg )
1909+
18521910 is_any = False
18531911 range_types = []
18541912 for r in self .slot_range_as_union (slot ):
@@ -1875,6 +1933,10 @@ def slot_range_as_union(self, slot: SlotDefinition) -> list[ElementName]:
18751933 :param slot:
18761934 :return: list of ranges
18771935 """
1936+ if not slot or not isinstance (slot , SlotDefinition ):
1937+ err_msg = "A SlotDefinition must be provided to generate the slot range as union."
1938+ raise ValueError (err_msg )
1939+
18781940 return list ({y .range for y in [slot , * [x for x in [* slot .exactly_one_of , * slot .any_of ] if x .range ]]})
18791941
18801942 def induced_slot_range (self , slot : SlotDefinition , strict : bool = False ) -> set [str | ElementName ]: # noqa: FBT001, FBT002
@@ -1892,6 +1954,9 @@ def induced_slot_range(self, slot: SlotDefinition, strict: bool = False) -> set[
18921954 :return: set of ranges
18931955 :rtype: set[str | ElementName]
18941956 """
1957+ if not slot or not isinstance (slot , SlotDefinition ):
1958+ err_msg = "A SlotDefinition must be provided to generate the induced slot range."
1959+ raise ValueError (err_msg )
18951960
18961961 slot_range = slot .range
18971962 any_of_range = {x .range for x in slot .any_of if x .range }
0 commit comments