Skip to content

Commit 080b48c

Browse files
authored
Merge pull request #179 from biolink/encapsulate-toolkit-Python-logger-message-management
Encapsulated management of ToolKit error messages
2 parents 5b039c0 + b2d779f commit 080b48c

3 files changed

Lines changed: 197 additions & 25 deletions

File tree

bmt/toolkit.py

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import requests
55
from functools import lru_cache, reduce
66

7-
from typing import List, Union, TextIO, Optional, Dict
7+
from typing import List, Union, TextIO, Optional, Dict, Set
88

99
from linkml_runtime.linkml_model import PermissibleValueText
1010
from linkml_runtime.utils.schemaview import SchemaView
@@ -23,9 +23,9 @@
2323

2424
LATEST_BIOLINK_RELEASE = "4.2.2"
2525

26-
REMOTE_PATH = f"https://raw.githubusercontent.com/biolink/biolink-model/v{LATEST_BIOLINK_RELEASE}/biolink-model.yaml"
27-
PREDICATE_MAP = f"https://raw.githubusercontent.com/biolink/biolink-model/v{LATEST_BIOLINK_RELEASE}/predicate_mapping.yaml"
28-
26+
BIOLINK_MODEL_RAW_BASEURL = f"https://raw.githubusercontent.com/biolink/biolink-model/v{LATEST_BIOLINK_RELEASE}/"
27+
REMOTE_PATH = f"{BIOLINK_MODEL_RAW_BASEURL}biolink-model.yaml"
28+
PREDICATE_MAP = f"{BIOLINK_MODEL_RAW_BASEURL}predicate_mapping.yaml"
2929

3030
NODE_PROPERTY = "node property"
3131
ASSOCIATION_SLOT = "association slot"
@@ -402,6 +402,99 @@ def match_association(
402402
return False
403403
return True
404404

405+
_warning_msg_templates: Dict[str, str] = {
406+
407+
"get_associations_subject_category":
408+
"Could not find subject category elements:\n\t'{ids}'\nwithin the current Biolink Model release?",
409+
410+
"get_associations_object_category":
411+
"Could not find object category elements:\n\t'{ids}'\nwithin the current Biolink Model release?",
412+
413+
"get_associations_predicate":
414+
"Could not find predicate elements:\n\t'{ids}'\nwithin the current Biolink Model release?",
415+
416+
"get_associations_no_predicate_inverse":
417+
"Predicates:\n\t'{ids}'\nare symmetric or lack an inverse, within the current Biolink Model release?",
418+
419+
"get_associations_missing_association":
420+
"Associations:\n\t'{ids}'\ndoes not match any association class within the current Biolink Model release?",
421+
422+
"get_element_by_prefix_missing_element":
423+
"No Biolink class found for the given curies:\n\t'{ids}'\n...try 'get_element_by_mapping'?"
424+
}
425+
426+
@classmethod
427+
def _format_warning_msg(cls, context: str, identifiers: Set[str]) -> str:
428+
"""
429+
Method to format warning messages associated with a
430+
specified element denoted by 'identifier',
431+
triggering the warning within a given functional context.
432+
433+
Parameters
434+
----------
435+
context: str
436+
Specific functional context for which the warning is being reported.
437+
identifiers: List[str]
438+
Specific element identifier targets about which the warning message is ussed.
439+
440+
Returns
441+
-------
442+
Formatted message string
443+
"""
444+
# sanity check
445+
assert context in cls._warning_msg_templates, f"Missing message template for context '{context}'?"
446+
447+
template: str = cls._warning_msg_templates[context]
448+
identifiers_str = ", ".join(identifiers)
449+
return f"{context} | {template.format(ids=identifiers_str)}"
450+
451+
# indexed list of identifiers captured in a given warning context
452+
_warning_id_catalog: Dict[str, Set[str]] = {}
453+
454+
@classmethod
455+
def warning(cls, context: str, identifier: str) -> None:
456+
"""
457+
Method to log warnings in a specified context and
458+
associated with a specific element, denoted by 'identifier'.
459+
460+
Parameters
461+
----------
462+
context: str
463+
Specific functional context for which the warning is being reported.
464+
identifier: str
465+
Specific element identifier target of the warning.
466+
467+
Returns
468+
-------
469+
None
470+
"""
471+
if context not in cls._warning_id_catalog:
472+
cls._warning_id_catalog[context] = set()
473+
identifiers: Set[str] = cls._warning_id_catalog.get(context, [])
474+
identifiers.add(identifier)
475+
476+
@classmethod
477+
def clear_warnings(cls) -> None:
478+
"""
479+
Clears out all warnings captured since initial
480+
Toolkit usage or since last invocation of this method.
481+
Returns
482+
-------
483+
None
484+
"""
485+
cls._warning_id_catalog.clear()
486+
487+
@classmethod
488+
def dump_warnings(cls) -> str:
489+
"""
490+
Dumps a flat list report by context of all warnings reported since
491+
Toolkit creation or since the last invocation of "clear_warnings'.
492+
"""
493+
report: str = ""
494+
for context, identifiers in cls._warning_id_catalog.items():
495+
report += cls._format_warning_msg(context=context, identifiers=identifiers)+"\n\n"
496+
return report
497+
405498
def get_associations(
406499
self,
407500
subject_categories: Optional[List[str]] = None,
@@ -458,9 +551,9 @@ def get_associations(
458551
for sc in subject_categories:
459552
sc_elem = self.get_element(sc)
460553
if not sc_elem:
461-
logger.warning(
462-
f"get_associations(): could not find subject category " +
463-
f"element '{str(sc)}' in current Biolink Model release?"
554+
self.warning(
555+
context="get_associations_subject_category",
556+
identifier=str(sc)
464557
)
465558
return []
466559
sc_formatted = format_element(sc_elem)
@@ -470,9 +563,9 @@ def get_associations(
470563
for oc in object_categories:
471564
oc_elem = self.get_element(oc)
472565
if not oc_elem:
473-
logger.warning(
474-
f"get_associations(): could not find object category " +
475-
f"element '{str(oc)}' in current Biolink Model release?"
566+
self.warning(
567+
context="get_associations_object_category",
568+
identifier=str(oc)
476569
)
477570
return []
478571
oc_formatted = format_element(oc_elem)
@@ -482,9 +575,9 @@ def get_associations(
482575
for pred in predicates:
483576
p_elem = self.get_element(pred)
484577
if not p_elem:
485-
logger.warning(
486-
f"get_associations(): could not find predicate " +
487-
f"element '{str(pred)}' in current Biolink Model release?"
578+
self.warning(
579+
context="get_associations_predicate",
580+
identifier=str(pred)
488581
)
489582
return []
490583
pred_formatted = format_element(p_elem)
@@ -501,9 +594,9 @@ def get_associations(
501594
inverse_p = self.get_inverse(p_elem.name)
502595
if not inverse_p:
503596
# might be a symmetrical predicate or a predicate lacking an inverse
504-
logger.warning(
505-
f"get_associations(): predicate '{str(p_elem.name)}' is symmetric or " +
506-
"does not have an inverse, within the current Biolink Model release?"
597+
self.warning(
598+
context="get_associations_no_predicate_inverse",
599+
identifier=str(p_elem.name)
507600
)
508601
else:
509602
inverse_pred_formatted = format_element(inverse_p)
@@ -522,9 +615,9 @@ def get_associations(
522615
if not association:
523616
# TODO: unsure that this test is needed, since all
524617
# known association classes ought to have names?
525-
logger.warning(
526-
f"get_associations(): association name '{str(name)}' " +
527-
f"does not match any element in the current Biolink Model release?"
618+
self.warning(
619+
context="get_associations_missing_association",
620+
identifier=str(name)
528621
)
529622
continue
530623

@@ -753,8 +846,9 @@ def get_permissible_value_parent(self, permissible_value: str, enum_name: str) -
753846
return parent
754847

755848
@lru_cache(CACHE_SIZE)
756-
def get_permissible_value_children(self, permissible_value: str, enum_name: str) -> Union[
757-
str, PermissibleValueText, None]:
849+
def get_permissible_value_children(
850+
self, permissible_value: str, enum_name: str
851+
) -> Union[str, PermissibleValueText, None]:
758852
"""
759853
Gets the children of a permissible value in an enumeration.
760854
@@ -970,9 +1064,9 @@ def get_element(self, name: str) -> Optional[Element]:
9701064
if el.name.lower() == name.lower():
9711065
element = el
9721066

973-
if type(element) == ClassDefinition and element.class_uri is None:
1067+
if isinstance(element, ClassDefinition) and element.class_uri is None:
9741068
element.class_uri = format_element(element)
975-
if type(element) == SlotDefinition and element.slot_uri is None:
1069+
if isinstance(element, SlotDefinition) and element.slot_uri is None:
9761070
element.slot_uri = format_element(element)
9771071
return element
9781072

@@ -1889,7 +1983,10 @@ def get_element_by_prefix(
18891983
if hasattr(element, 'id_prefixes') and prefix in element.id_prefixes:
18901984
categories.append(element.name)
18911985
if len(categories) == 0:
1892-
logger.warning("no biolink class found for the given curie: %s, try get_element_by_mapping?", identifier)
1986+
self.warning(
1987+
context="get_element_by_prefix_missing_element",
1988+
identifier=identifier
1989+
)
18931990

18941991
return categories
18951992

docs/intro/example_usage.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,44 @@ from bmt import Toolkit
117117
t = Toolkit('/path/to/biolink-model.yaml')
118118
```
119119

120-
The path can be a file path or a URL.
120+
The path can be a file path or a URL.
121+
122+
## Extraordinary Toolkit Warnings
123+
124+
The Toolkit tracks additional warnings generated about specific data elements by some methods like `get_associations` and `get_element_by_prefix`, but leaves it to the user to print them out.
125+
126+
These warnings are automatically tracked within the Toolkit and accessed by calling the dump function as follows:
127+
128+
```py
129+
from bmt import Toolkit
130+
from sys import stderr
131+
t = Toolkit()
132+
# calls made to `get_associations` and `get_element_by_prefix` with possible invalid elements...
133+
warnings: str = t.dump_warnings()
134+
print(warnings)
135+
```
136+
137+
should print something similar to this (obviously rather with the specific error contexts and identifiers which were seen)
138+
139+
```
140+
get_associations_object_category: Could not find object category elements:
141+
'biolink:NotACategory, NCBIGene:1010'
142+
within the current Biolink Model release?
143+
144+
get_element_by_prefix_missing_element: No Biolink class found for the given curies:
145+
'foo:bar'
146+
...try 'get_element_by_mapping'?
147+
148+
get_associations_missing_association: Associations:
149+
'biolink:NotAnAssociation'
150+
does not match any association class within the current Biolink Model release?
151+
```
152+
153+
You can reset the warning tracking log anytime as follows (the following assertion should work...)
154+
155+
```py
156+
157+
t.clear_warnings()
158+
warnings = t.dump_warnings()
159+
assert warnings == ""
160+
```

tests/unit/test_toolkit.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from typing import Optional, List, Dict
23

34
import pytest
@@ -93,6 +94,40 @@ def test_get_model_version(toolkit):
9394
assert version == LATEST_BIOLINK_RELEASE
9495

9596

97+
def test_warnings(toolkit):
98+
99+
# only certain contexts are legitimate
100+
with pytest.raises(AssertionError):
101+
toolkit._format_warning_msg(context="invalid-context", identifiers={"1", "2", "3"})
102+
103+
identifier = "HGNC:1234"
104+
context = "get_associations_subject_category"
105+
toolkit.warning(context=context, identifier=identifier)
106+
warnings: str = toolkit.dump_warnings()
107+
assert warnings.endswith(
108+
"get_associations_subject_category | "
109+
"Could not find subject category elements:\n\t'HGNC:1234'\n"
110+
"within the current Biolink Model release?\n\n"
111+
)
112+
113+
toolkit.clear_warnings()
114+
warnings = toolkit.dump_warnings()
115+
assert warnings == ""
116+
117+
# check the unit test console for a display of the following messages
118+
context = "get_associations_object_category"
119+
toolkit.warning(context=context, identifier="biolink:NotACategory")
120+
toolkit.warning(context=context, identifier="NCBIGene:1010")
121+
122+
context = "get_element_by_prefix_missing_element"
123+
toolkit.warning(context=context, identifier="foo:bar")
124+
125+
context = "get_associations_missing_association"
126+
toolkit.warning(context=context, identifier="biolink:NotAnAssociation")
127+
128+
print("\n\n"+toolkit.dump_warnings(), file=sys.stderr)
129+
130+
96131
def test_sv(toolkit):
97132
v = toolkit.view
98133
ancs = v.slot_ancestors('broad match')

0 commit comments

Comments
 (0)