diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 62d75730..96b5949d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -45,12 +45,6 @@ jobs: - name: Install library run: poetry install --no-interaction - #---------------------------------------------- - # run test suite - #---------------------------------------------- - - name: Run tests - run: poetry run python -m unittest discover - #---------------------------------------------- # coverage report #---------------------------------------------- diff --git a/linkml_runtime/__init__.py b/linkml_runtime/__init__.py index 9f93fcb7..6cc39aa1 100644 --- a/linkml_runtime/__init__.py +++ b/linkml_runtime/__init__.py @@ -25,6 +25,25 @@ MAIN_SCHEMA_PATH = SCHEMA_DIRECTORY / "meta.yaml" +LINKML_ANNOTATIONS = SCHEMA_DIRECTORY / "annotations.yaml" +LINKML_ARRAY = SCHEMA_DIRECTORY / "array.yaml" +LINKML_EXTENSIONS = SCHEMA_DIRECTORY / "extensions.yaml" +LINKML_MAPPINGS = SCHEMA_DIRECTORY / "mappings.yaml" +LINKML_TYPES = SCHEMA_DIRECTORY / "types.yaml" +LINKML_UNITS = SCHEMA_DIRECTORY / "units.yaml" +LINKML_VALIDATION = SCHEMA_DIRECTORY / "validation.yaml" + + +URI_TO_LOCAL = { + 'https://w3id.org/linkml/annotations.yaml': str(LINKML_ANNOTATIONS), + 'https://w3id.org/linkml/array.yaml': str(LINKML_ARRAY), + 'https://w3id.org/linkml/extensions.yaml': str(LINKML_EXTENSIONS), + 'https://w3id.org/linkml/mappings.yaml': str(LINKML_MAPPINGS), + 'https://w3id.org/linkml/meta.yaml': str(MAIN_SCHEMA_PATH), + 'https://w3id.org/linkml/types.yaml': str(LINKML_TYPES), + 'https://w3id.org/linkml/units.yaml': str(LINKML_UNITS), + 'https://w3id.org/linkml/validation.yaml': str(LINKML_VALIDATION), +} class MappingError(ValueError): """ diff --git a/linkml_runtime/exceptions.py b/linkml_runtime/exceptions.py new file mode 100644 index 00000000..97a7bfbb --- /dev/null +++ b/linkml_runtime/exceptions.py @@ -0,0 +1,2 @@ +class OrderingError(RuntimeError): + """Exception raised when there is a problem with SchemaView ordering""" \ No newline at end of file diff --git a/linkml_runtime/loaders/loader_root.py b/linkml_runtime/loaders/loader_root.py index 27506bf4..da4656ad 100644 --- a/linkml_runtime/loaders/loader_root.py +++ b/linkml_runtime/loaders/loader_root.py @@ -1,11 +1,16 @@ from abc import ABC, abstractmethod from typing import Iterator, TextIO, Union, Optional, Callable, Dict, Type, Any, List +from logging import getLogger from pydantic import BaseModel from hbreader import FileInfo, hbread from jsonasobj2 import as_dict, JsonObj from linkml_runtime.utils.yamlutils import YAMLRoot +from linkml_runtime import URI_TO_LOCAL + +CACHE_SIZE = 1024 + class Loader(ABC): @@ -128,7 +133,7 @@ def iter_instances(self) -> Iterator[Any]: pass - def _construct_target_class(self, + def _construct_target_class(self, data_as_dict: Union[dict, List[dict]], target_class: Union[Type[YAMLRoot], Type[BaseModel]]) -> Optional[Union[BaseModel, YAMLRoot, List[BaseModel], List[YAMLRoot]]]: if data_as_dict: @@ -151,6 +156,7 @@ def _construct_target_class(self, else: return None + def _read_source(self, source: Union[str, dict, TextIO], *, @@ -163,8 +169,17 @@ def _read_source(self, metadata.base_path = base_dir if not isinstance(source, dict): - data = hbread(source, metadata, metadata.base_path, accept_header) + # Try to get local version of schema, if one is known to exist + try: + if str(source) in URI_TO_LOCAL.keys(): + source = str(URI_TO_LOCAL[str(source)]) + except (TypeError, KeyError) as e: + # Fine, use original `source` value + logger = getLogger('linkml_runtime.loaders.Loader') + logger.debug(f"Error converting stringlike source to local linkml file: {source}, got: {e}") + + data = hbread(source, metadata, base_dir, accept_header) else: data = source - return data + return data \ No newline at end of file diff --git a/linkml_runtime/utils/eval_utils.py b/linkml_runtime/utils/eval_utils.py index 60c8d70a..962325f7 100644 --- a/linkml_runtime/utils/eval_utils.py +++ b/linkml_runtime/utils/eval_utils.py @@ -17,7 +17,12 @@ def eval_conditional(*conds: List[Tuple[bool, Any]]) -> Any: """ - >>> cond(x < 25 : 'low', x > 25 : 'high', True: 'low') + Evaluate a collection of expression,value tuples, returing the first value whose expression is true + + >>> x= 40 + >>> eval_conditional((x < 25, 'low'), (x > 25, 'high'), (True, 'low')) + 'high' + :param subj: :return: """ @@ -58,10 +63,9 @@ def eval_expr(expr: str, **kwargs) -> Any: Nulls: - - If a variable is enclosed in {}s then entire expression will eval to None if variable is unset + - If a variable is enclosed in {}s then entire expression will eval to None if any variable is unset - >>> eval_expr('{x} + {y}', x=None, y=2) - None + >>> assert eval_expr('{x} + {y}', x=None, y=2) is None Functions: @@ -92,9 +96,6 @@ def eval_expr(expr: str, **kwargs) -> Any: - - - def eval_(node, bindings={}): if isinstance(node, ast.Num): return node.n diff --git a/linkml_runtime/utils/namespaces.py b/linkml_runtime/utils/namespaces.py index a49a2143..a7232210 100644 --- a/linkml_runtime/utils/namespaces.py +++ b/linkml_runtime/utils/namespaces.py @@ -137,7 +137,9 @@ def _base(self) -> None: def curie_for(self, uri: Any, default_ok: bool = True, pythonform: bool = False) -> Optional[str]: """ Return the most appropriate CURIE for URI. The first longest matching prefix used, if any. If no CURIE is - present, None is returned + present, None is returned. + + Please see https://www.w3.org/TR/curie/ for more details about CURIEs. @param uri: URI to create the CURIE for @param default_ok: True means the default prefix is ok. Otherwise, we have to have a real prefix @@ -191,7 +193,7 @@ def prefix_for(self, uri_or_curie: Any, case_shift: bool = True) -> Optional[str def prefix_suffix(self, uri_or_curie: Any, case_shift: bool = True) -> Tuple[Optional[str], Optional[str]]: uri_or_curie = str(uri_or_curie) - if ':/' in uri_or_curie: + if '://' in uri_or_curie: uri_or_curie = self.curie_for(uri_or_curie) if not uri_or_curie: return None, None diff --git a/linkml_runtime/utils/schemaview.py b/linkml_runtime/utils/schemaview.py index 442ee53e..99fdd5cf 100644 --- a/linkml_runtime/utils/schemaview.py +++ b/linkml_runtime/utils/schemaview.py @@ -6,7 +6,7 @@ from copy import copy, deepcopy from collections import defaultdict, deque from pathlib import Path -from typing import Mapping, Tuple +from typing import Mapping, Tuple, TypeVar import warnings from linkml_runtime.utils.namespaces import Namespaces @@ -14,6 +14,7 @@ from linkml_runtime.utils.context_utils import parse_import_map, map_import from linkml_runtime.utils.pattern import PatternResolver from linkml_runtime.linkml_model.meta import * +from linkml_runtime.exceptions import OrderingError from enum import Enum logger = logging.getLogger(__name__) @@ -33,11 +34,23 @@ TYPE_NAME = Union[TypeDefinitionName, str] ENUM_NAME = Union[EnumDefinitionName, str] +ElementType = TypeVar("ElementType", bound=Element) +ElementNameType = TypeVar("ElementNameType", bound=Union[ElementName,str]) +DefinitionType = TypeVar("DefinitionType", bound=Definition) +DefinitionNameType = TypeVar("DefinitionNameType", bound=Union[DefinitionName,str]) +ElementDict = Dict[ElementNameType, ElementType] +DefDict = Dict[DefinitionNameType, DefinitionType] + class OrderedBy(Enum): RANK = "rank" LEXICAL = "lexical" PRESERVE = "preserve" + INHERITANCE = "inheritance" + """ + Order according to inheritance such that if C is a child of P then C appears after P + """ + def _closure(f, x, reflexive=True, depth_first=True, **kwargs): @@ -155,7 +168,7 @@ def __eq__(self, other): def __hash__(self): return hash(self.__key()) - @lru_cache() + @lru_cache(None) def namespaces(self) -> Namespaces: namespaces = Namespaces() for s in self.schema_map.values(): @@ -207,7 +220,7 @@ def load_import(self, imp: str, from_schema: SchemaDefinition = None): schema = load_schema_wrap(sname + '.yaml', base_dir=base_dir) return schema - @lru_cache() + @lru_cache(None) def imports_closure(self, imports: bool = True, traverse: Optional[bool] = None, inject_metadata=True) -> List[SchemaDefinitionName]: """ Return all imports @@ -287,7 +300,7 @@ def imports_closure(self, imports: bool = True, traverse: Optional[bool] = None, a.from_schema = s.id return closure - @lru_cache() + @lru_cache(None) def all_schema(self, imports: bool = True) -> List[SchemaDefinition]: """ :param imports: include imports closure @@ -297,7 +310,7 @@ def all_schema(self, imports: bool = True) -> List[SchemaDefinition]: return [m[sn] for sn in self.imports_closure(imports)] @deprecated("Use `all_classes` instead") - @lru_cache() + @lru_cache(None) def all_class(self, imports=True) -> Dict[ClassDefinitionName, ClassDefinition]: """ :param imports: include imports closure @@ -305,7 +318,22 @@ def all_class(self, imports=True) -> Dict[ClassDefinitionName, ClassDefinition]: """ return self._get_dict(CLASSES, imports) - def _order_lexically(self, elements: dict): + def ordered(self, elements: ElementDict, ordered_by: Optional[OrderedBy] = None) -> ElementDict: + """ + Order a dictionary of elements with some ordering method in :class:`.OrderedBy` + """ + if ordered_by in (OrderedBy.LEXICAL, OrderedBy.LEXICAL.value): + return self._order_lexically(elements) + elif ordered_by in (OrderedBy.RANK, OrderedBy.RANK.value): + return self._order_rank(elements) + elif ordered_by in (OrderedBy.INHERITANCE, OrderedBy.INHERITANCE.value): + return self._order_inheritance(elements) + elif ordered_by is None or ordered_by in (OrderedBy.PRESERVE, OrderedBy.PRESERVE.value): + return elements + else: + raise ValueError(f"ordered_by must be in OrderedBy or None, got {ordered_by}") + + def _order_lexically(self, elements: ElementDict) -> ElementDict: """ :param element: slots or class type to order :param imports @@ -320,7 +348,7 @@ def _order_lexically(self, elements: dict): ordered_elements[self.get_element(name).name] = self.get_element(name) return ordered_elements - def _order_rank(self, elements: dict): + def _order_rank(self, elements: ElementDict) -> ElementDict: """ :param elements: slots or classes to order :return: all classes or slots sorted by their rank in schema view @@ -342,7 +370,33 @@ def _order_rank(self, elements: dict): rank_ordered_elements.update(unranked_map) return rank_ordered_elements - @lru_cache() + def _order_inheritance(self, elements: DefDict) -> DefDict: + """ + sort classes such that if C is a child of P then C appears after P in the list + """ + clist = list(elements.values()) + slist = [] # sorted + can_add = False + while len(clist) > 0: + for i in range(len(clist)): + candidate = clist[i] + can_add = False + if candidate.is_a is None: + can_add = True + else: + if candidate.is_a in [p.name for p in slist]: + can_add = True + if can_add: + slist = slist + [candidate] + del clist[i] + break + if not can_add: + raise OrderingError(f"could not find suitable element in {clist} that does not ref {slist}") + + return {s.name: s for s in slist} + + + @lru_cache(None) def all_classes(self, ordered_by=OrderedBy.PRESERVE, imports=True) -> Dict[ClassDefinitionName, ClassDefinition]: """ :param ordered_by: an enumerated parameter that returns all the slots in the order specified. @@ -350,18 +404,11 @@ def all_classes(self, ordered_by=OrderedBy.PRESERVE, imports=True) -> Dict[Class :return: all classes in schema view """ classes = copy(self._get_dict(CLASSES, imports)) - - if ordered_by == OrderedBy.LEXICAL: - ordered_classes = self._order_lexically(elements=classes) - elif ordered_by == OrderedBy.RANK: - ordered_classes = self._order_rank(elements=classes) - else: # else preserve the order in the yaml - ordered_classes = classes - - return ordered_classes + classes = self.ordered(classes, ordered_by=ordered_by) + return classes @deprecated("Use `all_slots` instead") - @lru_cache() + @lru_cache(None) def all_slot(self, **kwargs) -> Dict[SlotDefinitionName, SlotDefinition]: """ :param imports: include imports closure @@ -369,7 +416,7 @@ def all_slot(self, **kwargs) -> Dict[SlotDefinitionName, SlotDefinition]: """ return self.all_slots(**kwargs) - @lru_cache() + @lru_cache(None) def all_slots(self, ordered_by=OrderedBy.PRESERVE, imports=True, attributes=True) -> Dict[ SlotDefinitionName, SlotDefinition]: """ @@ -386,17 +433,11 @@ def all_slots(self, ordered_by=OrderedBy.PRESERVE, imports=True, attributes=True if aname not in slots: slots[aname] = a - if ordered_by == OrderedBy.LEXICAL: - ordered_slots = self._order_lexically(elements=slots) - elif ordered_by == OrderedBy.RANK: - ordered_slots = self._order_rank(elements=slots) - else: - # preserve order in YAML - ordered_slots = slots - return ordered_slots + slots = self.ordered(slots, ordered_by=ordered_by) + return slots @deprecated("Use `all_enums` instead") - @lru_cache() + @lru_cache(None) def all_enum(self, imports=True) -> Dict[EnumDefinitionName, EnumDefinition]: """ :param imports: include imports closure @@ -404,7 +445,7 @@ def all_enum(self, imports=True) -> Dict[EnumDefinitionName, EnumDefinition]: """ return self._get_dict(ENUMS, imports) - @lru_cache() + @lru_cache(None) def all_enums(self, imports=True) -> Dict[EnumDefinitionName, EnumDefinition]: """ :param imports: include imports closure @@ -413,7 +454,7 @@ def all_enums(self, imports=True) -> Dict[EnumDefinitionName, EnumDefinition]: return self._get_dict(ENUMS, imports) @deprecated("Use `all_types` instead") - @lru_cache() + @lru_cache(None) def all_type(self, imports=True) -> Dict[TypeDefinitionName, TypeDefinition]: """ :param imports: include imports closure @@ -421,7 +462,7 @@ def all_type(self, imports=True) -> Dict[TypeDefinitionName, TypeDefinition]: """ return self._get_dict(TYPES, imports) - @lru_cache() + @lru_cache(None) def all_types(self, imports=True) -> Dict[TypeDefinitionName, TypeDefinition]: """ :param imports: include imports closure @@ -437,7 +478,7 @@ def all_subset(self, imports=True) -> Dict[SubsetDefinitionName, SubsetDefinitio """ return self._get_dict(SUBSETS, imports) - @lru_cache() + @lru_cache(None) def all_subsets(self, imports=True) -> Dict[SubsetDefinitionName, SubsetDefinition]: """ :param imports: include imports closure @@ -446,7 +487,7 @@ def all_subsets(self, imports=True) -> Dict[SubsetDefinitionName, SubsetDefiniti return self._get_dict(SUBSETS, imports) @deprecated("Use `all_elements` instead") - @lru_cache() + @lru_cache(None) def all_element(self, imports=True) -> Dict[ElementName, Element]: """ :param imports: include imports closure @@ -460,7 +501,7 @@ def all_element(self, imports=True) -> Dict[ElementName, Element]: # {**a,**b} syntax merges dictionary a and b into a single dictionary, removing duplicates. return {**all_classes, **all_slots, **all_enums, **all_types, **all_subsets} - @lru_cache() + @lru_cache(None) def all_elements(self, imports=True) -> Dict[ElementName, Element]: """ :param imports: include imports closure @@ -487,7 +528,7 @@ def _get_dict(self, slot_name: str, imports=True) -> Dict: return d - @lru_cache() + @lru_cache(None) def slot_name_mappings(self) -> Dict[str, SlotDefinition]: """ Mapping between processed safe slot names (following naming conventions) and slots. @@ -501,7 +542,7 @@ def slot_name_mappings(self) -> Dict[str, SlotDefinition]: m[underscore(s.name)] = s return m - @lru_cache() + @lru_cache(None) def class_name_mappings(self) -> Dict[str, ClassDefinition]: """ Mapping between processed safe class names (following naming conventions) and classes. @@ -515,7 +556,7 @@ def class_name_mappings(self) -> Dict[str, ClassDefinition]: m[camelcase(s.name)] = s return m - @lru_cache() + @lru_cache(None) def in_schema(self, element_name: ElementName) -> SchemaDefinitionName: """ :param element_name: @@ -526,7 +567,7 @@ def in_schema(self, element_name: ElementName) -> SchemaDefinitionName: raise ValueError(f'Element {element_name} not in any schema') return ix[element_name] - @lru_cache() + @lru_cache(None) def element_by_schema_map(self) -> Dict[ElementName, SchemaDefinitionName]: ix = {} schemas = self.all_schema(True) @@ -539,7 +580,7 @@ def element_by_schema_map(self) -> Dict[ElementName, SchemaDefinitionName]: ix[aname] = schema.name return ix - @lru_cache() + @lru_cache(None) def get_class(self, class_name: CLASS_NAME, imports=True, strict=False) -> ClassDefinition: """ :param class_name: name of the class to be retrieved @@ -552,7 +593,7 @@ def get_class(self, class_name: CLASS_NAME, imports=True, strict=False) -> Class else: return c - @lru_cache() + @lru_cache(None) def get_slot(self, slot_name: SLOT_NAME, imports=True, attributes=True, strict=False) -> SlotDefinition: """ :param slot_name: name of the slot to be retrieved @@ -575,7 +616,7 @@ def get_slot(self, slot_name: SLOT_NAME, imports=True, attributes=True, strict=F raise ValueError(f'No such slot as "{slot_name}"') return slot - @lru_cache() + @lru_cache(None) def get_subset(self, subset_name: SUBSET_NAME, imports=True, strict=False) -> SubsetDefinition: """ :param subset_name: name of the subsey to be retrieved @@ -588,7 +629,7 @@ def get_subset(self, subset_name: SUBSET_NAME, imports=True, strict=False) -> Su else: return s - @lru_cache() + @lru_cache(None) def get_enum(self, enum_name: ENUM_NAME, imports=True, strict=False) -> EnumDefinition: """ :param enum_name: name of the enum to be retrieved @@ -601,7 +642,7 @@ def get_enum(self, enum_name: ENUM_NAME, imports=True, strict=False) -> EnumDefi else: return e - @lru_cache() + @lru_cache(None) def get_type(self, type_name: TYPE_NAME, imports=True, strict=False) -> TypeDefinition: """ :param type_name: name of the type to be retrieved @@ -623,7 +664,7 @@ def _parents(self, e: Element, imports=True, mixins=True, is_a=True) -> List[Ele parents.append(e.is_a) return parents - @lru_cache() + @lru_cache(None) def class_parents(self, class_name: CLASS_NAME, imports=True, mixins=True, is_a=True) -> List[ClassDefinitionName]: """ :param class_name: child class name @@ -634,7 +675,7 @@ def class_parents(self, class_name: CLASS_NAME, imports=True, mixins=True, is_a= cls = self.get_class(class_name, imports, strict=True) return self._parents(cls, imports, mixins, is_a) - @lru_cache() + @lru_cache(None) def enum_parents(self, enum_name: ENUM_NAME, imports=False, mixins=False, is_a=True) -> List[EnumDefinitionName]: """ :param enum_name: child enum name @@ -645,7 +686,7 @@ def enum_parents(self, enum_name: ENUM_NAME, imports=False, mixins=False, is_a=T e = self.get_enum(enum_name, strict=True) return self._parents(e, imports, mixins, is_a=is_a) - @lru_cache() + @lru_cache(None) def permissible_value_parent(self, permissible_value: str, enum_name: ENUM_NAME) -> Union[ str, PermissibleValueText, None, ValueError]: """ @@ -662,7 +703,7 @@ def permissible_value_parent(self, permissible_value: str, enum_name: ENUM_NAME) else: return [] - @lru_cache() + @lru_cache(None) def permissible_value_children(self, permissible_value: str, enum_name: ENUM_NAME) -> Union[ str, PermissibleValueText, None, ValueError]: """ @@ -696,7 +737,7 @@ def permissible_value_children(self, permissible_value: str, enum_name: ENUM_NAM else: raise ValueError(f'No such enum as "{enum_name}"') - @lru_cache() + @lru_cache(None) def slot_parents(self, slot_name: SLOT_NAME, imports=True, mixins=True, is_a=True) -> List[SlotDefinitionName]: """ :param slot_name: child slot name @@ -710,7 +751,7 @@ def slot_parents(self, slot_name: SLOT_NAME, imports=True, mixins=True, is_a=Tru else: return [] - @lru_cache() + @lru_cache(None) def type_parents(self, type_name: TYPE_NAME, imports=True) -> List[TypeDefinitionName]: """ :param type_name: child type name @@ -723,7 +764,7 @@ def type_parents(self, type_name: TYPE_NAME, imports=True) -> List[TypeDefinitio else: return [] - @lru_cache() + @lru_cache(None) def get_children(self, name: str, mixin: bool = True) -> List[str]: """ get the children of an element (any class, slot, enum, type) @@ -740,7 +781,7 @@ def get_children(self, name: str, mixin: bool = True) -> List[str]: children.append(el.name) return children - @lru_cache() + @lru_cache(None) def class_children(self, class_name: CLASS_NAME, imports=True, mixins=True, is_a=True) -> List[ClassDefinitionName]: """ :param class_name: parent class name @@ -752,7 +793,7 @@ def class_children(self, class_name: CLASS_NAME, imports=True, mixins=True, is_a elts = [self.get_class(x) for x in self.all_classes(imports=imports)] return [x.name for x in elts if (x.is_a == class_name and is_a) or (mixins and class_name in x.mixins)] - @lru_cache() + @lru_cache(None) def slot_children(self, slot_name: SLOT_NAME, imports=True, mixins=True, is_a=True) -> List[SlotDefinitionName]: """ :param slot_name: parent slot name @@ -764,7 +805,7 @@ def slot_children(self, slot_name: SLOT_NAME, imports=True, mixins=True, is_a=Tr elts = [self.get_slot(x) for x in self.all_slots(imports)] return [x.name for x in elts if (x.is_a == slot_name and is_a) or (mixins and slot_name in x.mixins)] - @lru_cache() + @lru_cache(None) def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True, depth_first=True) -> List[ClassDefinitionName]: """ @@ -782,7 +823,7 @@ def class_ancestors(self, class_name: CLASS_NAME, imports=True, mixins=True, ref class_name, reflexive=reflexive, depth_first=depth_first) - @lru_cache() + @lru_cache(None) def permissible_value_ancestors(self, permissible_value_text: str, enum_name: ENUM_NAME, reflexive=True, @@ -797,7 +838,7 @@ def permissible_value_ancestors(self, permissible_value_text: str, reflexive=reflexive, depth_first=depth_first) - @lru_cache() + @lru_cache(None) def permissible_value_descendants(self, permissible_value_text: str, enum_name: ENUM_NAME, reflexive=True, @@ -813,7 +854,7 @@ def permissible_value_descendants(self, permissible_value_text: str, reflexive=reflexive, depth_first=depth_first) - @lru_cache() + @lru_cache(None) def enum_ancestors(self, enum_name: ENUM_NAME, imports=True, mixins=True, reflexive=True, is_a=True, depth_first=True) -> List[EnumDefinitionName]: """ @@ -831,7 +872,7 @@ def enum_ancestors(self, enum_name: ENUM_NAME, imports=True, mixins=True, reflex enum_name, reflexive=reflexive, depth_first=depth_first) - @lru_cache() + @lru_cache(None) def type_ancestors(self, type_name: TYPES, imports=True, reflexive=True, depth_first=True) -> List[ TypeDefinitionName]: """ @@ -847,7 +888,7 @@ def type_ancestors(self, type_name: TYPES, imports=True, reflexive=True, depth_f type_name, reflexive=reflexive, depth_first=depth_first) - @lru_cache() + @lru_cache(None) def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ SlotDefinitionName]: """ @@ -864,7 +905,7 @@ def slot_ancestors(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflex slot_name, reflexive=reflexive) - @lru_cache() + @lru_cache(None) def class_descendants(self, class_name: CLASS_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ ClassDefinitionName]: """ @@ -880,7 +921,7 @@ def class_descendants(self, class_name: CLASS_NAME, imports=True, mixins=True, r return _closure(lambda x: self.class_children(x, imports=imports, mixins=mixins, is_a=is_a), class_name, reflexive=reflexive) - @lru_cache() + @lru_cache(None) def slot_descendants(self, slot_name: SLOT_NAME, imports=True, mixins=True, reflexive=True, is_a=True) -> List[ SlotDefinitionName]: """ @@ -896,7 +937,7 @@ def slot_descendants(self, slot_name: SLOT_NAME, imports=True, mixins=True, refl return _closure(lambda x: self.slot_children(x, imports=imports, mixins=mixins, is_a=is_a), slot_name, reflexive=reflexive) - @lru_cache() + @lru_cache(None) def class_roots(self, imports=True, mixins=True, is_a=True) -> List[ClassDefinitionName]: """ All classes that have no parents @@ -909,7 +950,7 @@ def class_roots(self, imports=True, mixins=True, is_a=True) -> List[ClassDefinit for c in self.all_classes(imports=imports) if self.class_parents(c, mixins=mixins, is_a=is_a, imports=imports) == []] - @lru_cache() + @lru_cache(None) def class_leaves(self, imports=True, mixins=True, is_a=True) -> List[ClassDefinitionName]: """ All classes that have no children @@ -922,7 +963,7 @@ def class_leaves(self, imports=True, mixins=True, is_a=True) -> List[ClassDefini for c in self.all_classes(imports=imports) if self.class_children(c, mixins=mixins, is_a=is_a, imports=imports) == []] - @lru_cache() + @lru_cache(None) def slot_roots(self, imports=True, mixins=True) -> List[SlotDefinitionName]: """ All slotes that have no parents @@ -934,7 +975,7 @@ def slot_roots(self, imports=True, mixins=True) -> List[SlotDefinitionName]: for c in self.all_slots(imports=imports) if self.slot_parents(c, mixins=mixins, imports=imports) == []] - @lru_cache() + @lru_cache(None) def slot_leaves(self, imports=True, mixins=True) -> List[SlotDefinitionName]: """ All slotes that have no children @@ -946,7 +987,7 @@ def slot_leaves(self, imports=True, mixins=True) -> List[SlotDefinitionName]: for c in self.all_slots(imports=imports) if self.slot_children(c, mixins=mixins, imports=imports) == []] - @lru_cache() + @lru_cache(None) def is_multivalued(self, slot_name: SlotDefinition) -> bool: """ returns True if slot is multivalued, else returns False @@ -956,7 +997,7 @@ def is_multivalued(self, slot_name: SlotDefinition) -> bool: induced_slot = self.induced_slot(slot_name) return True if induced_slot.multivalued else False - @lru_cache() + @lru_cache(None) def slot_is_true_for_metadata_property(self, slot_name: SlotDefinition, metadata_property: str) -> bool: """ Returns true if the value of the provided "metadata_property" is True. For example, @@ -1082,7 +1123,7 @@ def get_elements_applicable_by_prefix(self, prefix: str) -> List[str]: return applicable_elements - @lru_cache() + @lru_cache(None) def all_aliases(self) -> List[str]: """ Get the aliases @@ -1103,7 +1144,7 @@ def all_aliases(self) -> List[str]: return element_aliases - @lru_cache() + @lru_cache(None) def get_mappings(self, element_name: ElementName = None, imports=True, expand=False) -> Dict[ MAPPING_TYPE, List[URIorCURIE]]: """ @@ -1134,7 +1175,7 @@ def get_mappings(self, element_name: ElementName = None, imports=True, expand=Fa return m_dict - @lru_cache() + @lru_cache(None) def is_mixin(self, element_name: Union[ElementName, Element]): """ Determines whether the given name is the name of a mixin @@ -1148,7 +1189,7 @@ def is_mixin(self, element_name: Union[ElementName, Element]): is_mixin = element.mixin if isinstance(element, Definition) else False return is_mixin - @lru_cache() + @lru_cache(None) def inverse(self, slot_name: SlotDefinition): """ Determines whether the given name is a relationship, and if that relationship has an inverse, returns @@ -1192,7 +1233,7 @@ def get_mapping_index(self, imports=True, expand=False) -> Dict[URIorCURIE, List ix[v].append((mapping_type, self.get_element(en, imports=imports))) return ix - @lru_cache() + @lru_cache(None) def is_relationship(self, class_name: CLASS_NAME = None, imports=True) -> bool: """ Tests if a class represents a relationship or reified statement @@ -1211,7 +1252,7 @@ def is_relationship(self, class_name: CLASS_NAME = None, imports=True) -> bool: return True return False - @lru_cache() + @lru_cache(None) def annotation_dict(self, element_name: ElementName, imports=True) -> Dict[URIorCURIE, Any]: """ Return a dictionary where keys are annotation tags and values are annotation values for any given element. @@ -1227,7 +1268,7 @@ def annotation_dict(self, element_name: ElementName, imports=True) -> Dict[URIor e = self.get_element(element_name, imports=imports) return {k: v.value for k, v in e.annotations.items()} - @lru_cache() + @lru_cache(None) def class_slots(self, class_name: CLASS_NAME, imports=True, direct=False, attributes=True) -> List[ SlotDefinitionName]: """ @@ -1253,7 +1294,7 @@ def class_slots(self, class_name: CLASS_NAME, imports=True, direct=False, attrib slots_nr.append(s) return slots_nr - @lru_cache() + @lru_cache(None) def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, imports=True, mangle_name=False) -> SlotDefinition: """ @@ -1351,12 +1392,12 @@ def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, impo induced_slot.domain_of.append(c.name) return induced_slot - @lru_cache() + @lru_cache(None) def _metaslots_for_slot(self): fake_slot = SlotDefinition('__FAKE') return vars(fake_slot).keys() - @lru_cache() + @lru_cache(None) def class_induced_slots(self, class_name: CLASS_NAME = None, imports=True) -> List[SlotDefinition]: """ All slots that are asserted or inferred for a class, with their inferred semantics @@ -1367,7 +1408,7 @@ def class_induced_slots(self, class_name: CLASS_NAME = None, imports=True) -> Li """ return [self.induced_slot(sn, class_name, imports=imports) for sn in self.class_slots(class_name)] - @lru_cache() + @lru_cache(None) def induced_class(self, class_name: CLASS_NAME = None) -> ClassDefinition: """ Generate an induced class @@ -1387,7 +1428,7 @@ def induced_class(self, class_name: CLASS_NAME = None) -> ClassDefinition: c.slots = [] return c - @lru_cache() + @lru_cache(None) def induced_type(self, type_name: TYPE_NAME = None) -> TypeDefinition: """ @@ -1405,7 +1446,7 @@ def induced_type(self, type_name: TYPE_NAME = None) -> TypeDefinition: t.repr = parent.repr return t - @lru_cache() + @lru_cache(None) def induced_enum(self, enum_name: ENUM_NAME = None) -> EnumDefinition: """ @@ -1415,7 +1456,7 @@ def induced_enum(self, enum_name: ENUM_NAME = None) -> EnumDefinition: e = deepcopy(self.get_enum(enum_name)) return e - @lru_cache() + @lru_cache(None) def get_identifier_slot(self, cn: CLASS_NAME, use_key=False, imports=True) -> Optional[SlotDefinition]: """ Find the slot that is the identifier for the given class @@ -1433,7 +1474,7 @@ def get_identifier_slot(self, cn: CLASS_NAME, use_key=False, imports=True) -> Op else: return None - @lru_cache() + @lru_cache(None) def get_key_slot(self, cn: CLASS_NAME, imports=True) -> Optional[SlotDefinition]: """ Find the slot that is the key for the given class @@ -1448,7 +1489,7 @@ def get_key_slot(self, cn: CLASS_NAME, imports=True) -> Optional[SlotDefinition] return s return None - @lru_cache() + @lru_cache(None) def get_type_designator_slot(self, cn: CLASS_NAME, imports=True) -> Optional[SlotDefinition]: """ :param cn: class name @@ -1558,7 +1599,7 @@ def get_classes_by_slot( return list(classes_set) - @lru_cache() + @lru_cache(None) def get_slots_by_enum(self, enum_name: ENUM_NAME = None) -> List[SlotDefinition]: """Get all slots that use a given enum: schema defined, attribute, or slot_usage. @@ -1609,7 +1650,7 @@ def is_slot_percent_encoded(self, slot: SlotDefinitionName) -> bool: anns = self.get_type(t).annotations return "percent_encoded" in anns - @lru_cache() + @lru_cache(None) def usage_index(self) -> Dict[ElementName, List[SchemaUsage]]: """ Fetch an index that shows the ways in which each element is used diff --git a/linkml_runtime/utils/yamlutils.py b/linkml_runtime/utils/yamlutils.py index bf3a5bca..a4b0677f 100644 --- a/linkml_runtime/utils/yamlutils.py +++ b/linkml_runtime/utils/yamlutils.py @@ -14,6 +14,11 @@ YAMLObjTypes = Union[JsonObjTypes, "YAMLRoot"] +try: + from yaml import CSafeLoader as SafeLoader +except ImportError: + from yaml.loader import SafeLoader + class YAMLMark(yaml.error.Mark): def __str__(self): @@ -369,7 +374,7 @@ class extended_float(float, TypedNode): pass -class DupCheckYamlLoader(yaml.loader.SafeLoader): +class DupCheckYamlLoader(SafeLoader): """ A YAML loader that throws an error when the same key appears twice """ diff --git a/tests/test_issues/input/issue_1040.yaml b/tests/test_issues/input/issue_1040.yaml index dbe42d4e..a436c631 100644 --- a/tests/test_issues/input/issue_1040.yaml +++ b/tests/test_issues/input/issue_1040.yaml @@ -17,4 +17,4 @@ classes: Person: in_subset: - a - - + - \ No newline at end of file diff --git a/tests/test_issues/test_issue_1040.py b/tests/test_issues/test_issue_1040.py index fd4fa5f9..d51d1352 100644 --- a/tests/test_issues/test_issue_1040.py +++ b/tests/test_issues/test_issue_1040.py @@ -19,4 +19,4 @@ def test_issue_1040_file_name(self): trace. We use this to make sure that the file name gets in correctly. """ with self.assertRaises(yaml.constructor.ConstructorError) as e: yaml_loader.load(env.input_path('issue_1040.yaml'), SchemaDefinition) - self.assertIn('File "issue_1040.yaml"', str(e.exception)) + self.assertIn('"issue_1040.yaml"', str(e.exception)) diff --git a/tests/test_utils/test_eval_utils.py b/tests/test_utils/test_eval_utils.py index 4bc906cf..500d79ac 100644 --- a/tests/test_utils/test_eval_utils.py +++ b/tests/test_utils/test_eval_utils.py @@ -1,7 +1,8 @@ -import unittest from dataclasses import dataclass from typing import List, Dict +import pytest + from linkml_runtime.utils.eval_utils import eval_expr @@ -24,90 +25,58 @@ class Container: person_index: Dict[str, Person] = None -class EvalUtilsTestCase(unittest.TestCase): - """ - Tests for linkml_runtime.utils.eval_utils - """ - - def test_eval_expressions(self): - """ - Tests evaluation of expressions using eval_expr - """ - x = eval_expr("1 + 2") - self.assertEqual(x, 3) - self.assertEqual(eval_expr("1 + 2 + 3"), 6) - x = eval_expr("{z} + 2", z=1) - self.assertEqual(x, 3) - self.assertIsNone(eval_expr('{x} + {y}', x=5, y=None)) - x = eval_expr("'x' + 'y'") - assert x == 'xy' - #x = eval_expr("'{x}' + '{y}'", x='a', y='b') - #self.assertEqual(x, 'ab') - self.assertEqual(eval_expr("['a','b'] + ['c','d']"), ['a', 'b', 'c', 'd']) - self.assertEqual(eval_expr("{x} + {y}", x=['a', 'b'], y=['c', 'd']), ['a', 'b', 'c', 'd']) - self.assertEqual(eval_expr("{'a': 1}"), {'a': 1}) - self.assertEqual(eval_expr("max([1, 5, 2])"), 5) - self.assertEqual(eval_expr("max({x})", x=[1, 5, 2]), 5) - self.assertEqual(eval_expr("True"), True) - self.assertEqual(eval_expr("False"), False) - self.assertEqual(eval_expr("1 + 1 == 3"), False) - self.assertEqual(eval_expr("1 < 2"), True) - self.assertEqual(eval_expr("1 <= 1"), True) - self.assertEqual(eval_expr("1 >= 1"), True) - self.assertEqual(eval_expr("2 > 1"), True) - self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=1), 'EQ') - self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2), 'NEQ') - self.assertEqual(eval_expr("'NOT_NULL' if x else 'NULL'", x=1), 'NOT_NULL') - self.assertEqual(eval_expr("'NOT_NULL' if x else 'NULL'", x=None), 'NULL') - self.assertEqual(eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2), 'NEQ') - case = "case(({x} < 25, 'LOW'), ({x} > 75, 'HIGH'), (True, 'MEDIUM'))" - self.assertEqual(eval_expr(case, x=10), 'LOW') - self.assertEqual(eval_expr(case, x=100), 'HIGH') - self.assertEqual(eval_expr(case, x=50), 'MEDIUM') - self.assertEqual(eval_expr('x', x='a'), 'a') - self.assertEqual(eval_expr('x+y', x=1, y=2), 3) - # todo - self.assertEqual(eval_expr('x["a"] + y', x={'a': 1}, y=2), 3) - self.assertEqual(eval_expr('x["a"]["b"] + y', x={'a': {'b': 1}}, y=2), 3) - p = Person(name='x', aliases=['a', 'b', 'c'], address=Address(street='1 x street')) - self.assertEqual(eval_expr('p.name', p=p), 'x') - self.assertEqual(eval_expr('p.address.street', p=p), '1 x street') - self.assertEqual(eval_expr('len(p.aliases)', p=p), 3) - self.assertEqual(eval_expr('p.aliases', p=p), p.aliases) - p2 = Person(name='x2', aliases=['a2', 'b2', 'c2'], address=Address(street='2 x street')) - c = Container(persons=[p, p2]) - x = eval_expr('c.persons.name', c=c) - self.assertEqual(x, ['x', 'x2']) - x = eval_expr('c.persons.address.street', c=c) - self.assertEqual(x, ['1 x street', '2 x street']) - x = eval_expr('strlen(c.persons.address.street)', c=c) - self.assertEqual(x, [10, 10]) - c = Container(person_index={p.name: p, p2.name: p2}) - x = eval_expr('c.person_index.name', c=c) - #print(x) - self.assertEqual(x, ['x', 'x2']) - x = eval_expr('c.person_index.address.street', c=c) - self.assertEqual(x, ['1 x street', '2 x street']) - x = eval_expr('strlen(c.person_index.name)', c=c) - self.assertEqual(x, [1, 2]) - #self.assertEqual('x', eval_expr('"x" if True else "y"')) - - def test_no_eval_prohibited(self): - """ - Ensure that certain patterns cannot be evaluated +def test_eval_expressions(): + assert eval_expr("1 + 2") == 3 + assert eval_expr("1 + 2 + 3") == 6 + assert eval_expr("{z} + 2", z=1) == 3 + assert eval_expr('{x} + {y}', x=5, y=None) is None + assert eval_expr("'x' + 'y'") == 'xy' + assert eval_expr("['a','b'] + ['c','d']") == ['a', 'b', 'c', 'd'] + assert eval_expr("{x} + {y}", x=['a', 'b'], y=['c', 'd']) == ['a', 'b', 'c', 'd'] + assert eval_expr("{'a': 1}") == {'a': 1} + assert eval_expr("max([1, 5, 2])") == 5 + assert eval_expr("max({x})", x=[1, 5, 2]) == 5 + assert eval_expr("True") is True + assert eval_expr("False") is False + assert eval_expr("1 + 1 == 3") is False + assert eval_expr("1 < 2") is True + assert eval_expr("1 <= 1") is True + assert eval_expr("1 >= 1") is True + assert eval_expr("2 > 1") is True + assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=1) == 'EQ' + assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2) == 'NEQ' + assert eval_expr("'NOT_NULL' if x else 'NULL'", x=1) == 'NOT_NULL' + assert eval_expr("'NOT_NULL' if x else 'NULL'", x=None) == 'NULL' + assert eval_expr("'EQ' if {x} == {y} else 'NEQ'", x=1, y=2) == 'NEQ' + case = "case(({x} < 25, 'LOW'), ({x} > 75, 'HIGH'), (True, 'MEDIUM'))" + assert eval_expr(case, x=10) == 'LOW' + assert eval_expr(case, x=100) == 'HIGH' + assert eval_expr(case, x=50) == 'MEDIUM' + assert eval_expr('x', x='a') == 'a' + assert eval_expr('x+y', x=1, y=2) == 3 + assert eval_expr('x["a"] + y', x={'a': 1}, y=2) == 3 + assert eval_expr('x["a"]["b"] + y', x={'a': {'b': 1}}, y=2) == 3 + p = Person(name='x', aliases=['a', 'b', 'c'], address=Address(street='1 x street')) + assert eval_expr('p.name', p=p) == 'x' + assert eval_expr('p.address.street', p=p) == '1 x street' + assert eval_expr('len(p.aliases)', p=p) == 3 + assert eval_expr('p.aliases', p=p) == p.aliases + p2 = Person(name='x2', aliases=['a2', 'b2', 'c2'], address=Address(street='2 x street')) + c = Container(persons=[p, p2]) + assert eval_expr('c.persons.name', c=c) == ['x', 'x2'] + assert eval_expr('c.persons.address.street', c=c) == ['1 x street', '2 x street'] + assert eval_expr('strlen(c.persons.address.street)', c=c) == [10, 10] + c = Container(person_index={p.name: p, p2.name: p2}) + assert eval_expr('c.person_index.name', c=c) == ['x', 'x2'] + assert eval_expr('c.person_index.address.street', c=c) == ['1 x street', '2 x street'] + assert eval_expr('strlen(c.person_index.name)', c=c) == [1, 2] - See ``_ - """ - with self.assertRaises(NotImplementedError): - eval_expr("__import__('os').listdir()") - def test_funcs(self): - """ - Not yet implemented - """ - with self.assertRaises(NotImplementedError): - eval_expr("my_func([1,2,3])") +def test_no_eval_prohibited(): + with pytest.raises(NotImplementedError): + eval_expr("__import__('os').listdir()") -if __name__ == '__main__': - unittest.main() +def test_funcs(): + with pytest.raises(NotImplementedError): + eval_expr("my_func([1,2,3])") \ No newline at end of file diff --git a/tests/test_utils/test_namespaces.py b/tests/test_utils/test_namespaces.py index bd90c7f7..fe08fc91 100644 --- a/tests/test_utils/test_namespaces.py +++ b/tests/test_utils/test_namespaces.py @@ -14,7 +14,7 @@ def test_namespaces(self): self.assertEqual(str(ns.skos), str(SKOS)) self.assertEqual(ns.skos.note, SKOS.note) ns.OIO = URIRef("http://www.geneontology.org/formats/oboInOwl") - ns['dc'] = "http://example.org/dc/" # Overrides 'dc' in semweb_context + ns['dc'] = "http://example.org/dc/" # Overrides 'dc' in semweb_context ns['l1'] = "http://example.org/subset/" ns['l2'] = "http://example.org/subset/test/" ns['l3'] = "http://example.org/subset/t" @@ -63,8 +63,8 @@ def test_namespaces(self): self.assertEqual('u1:foo', ns.curie_for("urn:example:foo")) with self.assertRaises(ValueError): ns.curie_for("1abc\junk") - #no comment in skos? - #self.assertEqual(SKOS.comment, ns.uri_for("skos:comment")) + # no comment in skos? + # self.assertEqual(SKOS.comment, ns.uri_for("skos:comment")) self.assertEqual(URIRef('http://example.org/dc/table'), ns.uri_for("dc:table")) self.assertEqual(ns.uri_for("http://something.org"), URIRef("http://something.org")) self.assertEqual('https://w3id.org/biolink/metamodel/Schema', str(ns.uri_for(":Schema"))) @@ -100,5 +100,21 @@ def test_prefixmaps_integration(self): self.assertEqual(prefixmap_merged.curie_for(test_NCIT_uri), test_NCIT_curie) self.assertEqual(prefixmap_merged.uri_for(test_NCIT_curie), test_NCIT_uri) + def test_prefix_suffix(self): + ns = Namespaces() + ns['farm'] = 'https://example.org/farm' + ns['farm_slash'] = 'https://slash.org/farm/' + + self.assertEqual(('farm', 'cow'), ns.prefix_suffix('farm:cow')) + self.assertEqual(('farm', '/cow'), ns.prefix_suffix('https://example.org/farm/cow')) + self.assertEqual(('farm_slash', 'cow'), ns.prefix_suffix('https://slash.org/farm/cow')) + self.assertEqual(('farm_slash', 'cow/horns'), ns.prefix_suffix('farm_slash:cow/horns')) + self.assertEqual(('farm', '/cow/horns'), ns.prefix_suffix('farm:/cow/horns')) + self.assertEqual(('farm', '#cow/horns'), ns.prefix_suffix('farm:#cow/horns')) + self.assertEqual(('farm', ''), ns.prefix_suffix('farm:')) + self.assertEqual(('', 'cow'), ns.prefix_suffix(':cow')) + self.assertEqual((None, None), ns.prefix_suffix('https://missing-prefix.org/farm/cow')) + + if __name__ == '__main__': unittest.main()