From 798591b29661efa8fef05703c79e3614d40f911d Mon Sep 17 00:00:00 2001 From: Sujay Patil Date: Wed, 31 Jul 2024 16:37:45 -0700 Subject: [PATCH 1/6] induced_slot() now materializes non-scalar metaslots --- linkml_runtime/utils/schemaview.py | 6 +- .../test_issues/input/linkml_issue_2224.yaml | 62 +++++++++++++++++++ tests/test_issues/test_issue_2224.py | 52 ++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 tests/test_issues/input/linkml_issue_2224.yaml create mode 100644 tests/test_issues/test_issue_2224.py diff --git a/linkml_runtime/utils/schemaview.py b/linkml_runtime/utils/schemaview.py index 504bee44..829729fc 100644 --- a/linkml_runtime/utils/schemaview.py +++ b/linkml_runtime/utils/schemaview.py @@ -12,6 +12,7 @@ from linkml_runtime.utils.namespaces import Namespaces from deprecated.classic import deprecated from linkml_runtime.utils.context_utils import parse_import_map, map_import +from linkml_runtime.utils.formatutils import is_empty from linkml_runtime.utils.pattern import PatternResolver from linkml_runtime.linkml_model.meta import * from linkml_runtime.exceptions import OrderingError @@ -1369,7 +1370,10 @@ def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, impo if v2 is not None: v = COMBINE[metaslot_name](v, v2) else: - if v2 is not None: + # can rewrite below as: + # 1. if v2: + # 2. if v2 is not None and ((isinstance(v2, (dict, list)) and v2) or (isinstance(v2, JsonObj) and as_dict(v2))) + if not is_empty(v2): v = v2 logging.debug(f'{v} takes precedence over {v2} for {induced_slot.name}.{metaslot_name}') if v is None: diff --git a/tests/test_issues/input/linkml_issue_2224.yaml b/tests/test_issues/input/linkml_issue_2224.yaml new file mode 100644 index 00000000..1adc807e --- /dev/null +++ b/tests/test_issues/input/linkml_issue_2224.yaml @@ -0,0 +1,62 @@ +id: DJControllerSchema +name: DJControllerSchema +title: LinkML schema for my DJ controller +imports: +- linkml:types +classes: + DJController: + slots: + - jog_wheels + - tempo + - volume_faders + - crossfaders + slot_usage: + tempo: + examples: + - value: 120.0 + - value: 144.0 + - value: 126.8 + - value: 102.6 + annotations: + expected_value: a number between 0 and 300 + preferred_unit: BPM +slots: + jog_wheels: + description: The number of jog wheels on the DJ controller + range: integer + examples: + - value: 2 + annotations: + expected_value: an integer between 0 and 4 + in_subset: decks + tempo: + description: The tempo of the track (in BPM) + range: float + examples: + - value: 120.0 + - value: 144.0 + annotations: + expected_value: a number between 0 and 200 + preferred_unit: BPM + in_subset: decks + volume_faders: + description: The number of volume faders on the DJ controller + range: integer + examples: + - value: 4 + annotations: + expected_value: an integer between 0 and 8 + in_subset: mixer + crossfaders: + description: The number of crossfaders on the DJ controller + range: integer + examples: + - value: 1 + annotations: + expected_value: an integer between 0 and 2 + in_subset: mixer +subsets: + decks: + description: A subset that represents the components in the deck portion of a DJ controller + mixer: + description: A subset that represents the components in the mixer portion of a DJ controller diff --git a/tests/test_issues/test_issue_2224.py b/tests/test_issues/test_issue_2224.py new file mode 100644 index 00000000..b570679e --- /dev/null +++ b/tests/test_issues/test_issue_2224.py @@ -0,0 +1,52 @@ +import unittest +from unittest import TestCase +from linkml_runtime.utils.schemaview import SchemaView +from jsonasobj2 import JsonObj + +from tests.test_issues.environment import env + + +class Issue2224TestCase(TestCase): + env = env + + def test_issue_2224_slot_classes(self): + sv = SchemaView(env.input_path("linkml_issue_2224.yaml")) + cls = sv.induced_class("DJController") + + # jog_wheels is a slot asserted at the schema level + # check that the range (scalar value) is being materialized properly + self.assertEqual(cls.attributes["jog_wheels"].range, "integer") + # check that the examples (list) is being materialized properly + self.assertIsInstance(cls.attributes["jog_wheels"].examples, list) + for example in cls.attributes["jog_wheels"].examples: + self.assertEqual(example.value, "2") + for example in cls.attributes["volume_faders"].examples: + self.assertEqual(example.value, "4") + for example in cls.attributes["crossfaders"].examples: + self.assertEqual(example.value, "1") + # check that the annotations (dictionary) is being materialized properly + self.assertIsInstance(cls.attributes["jog_wheels"].annotations, JsonObj) + self.assertEqual( + cls.attributes["jog_wheels"].annotations.expected_value.value, + "an integer between 0 and 4", + ) + self.assertEqual( + cls.attributes["volume_faders"].annotations.expected_value.value, + "an integer between 0 and 8", + ) + + # examples being overriden by slot_usage modification + for example in cls.attributes["tempo"].examples: + self.assertIn(example.value, ["120.0", "144.0", "126.8", "102.6"]) + # annotations being overriden by slot_usage modification + self.assertEqual( + cls.attributes["tempo"].annotations.expected_value.value, + "a number between 0 and 300", + ) + self.assertEqual( + cls.attributes["tempo"].annotations.preferred_unit.value, "BPM" + ) + + +if __name__ == "__main__": + unittest.main() From 672c80d83e391b67211554d7788dc65d0c9f198f Mon Sep 17 00:00:00 2001 From: Sujay Patil Date: Thu, 1 Aug 2024 12:20:12 -0700 Subject: [PATCH 2/6] test for exact values in examples under `tempo` slot in `linkml_issue_2224.yaml` Co-authored-by: Jonny Saunders --- tests/test_issues/test_issue_2224.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_issues/test_issue_2224.py b/tests/test_issues/test_issue_2224.py index b570679e..eeb77fcf 100644 --- a/tests/test_issues/test_issue_2224.py +++ b/tests/test_issues/test_issue_2224.py @@ -36,8 +36,7 @@ def test_issue_2224_slot_classes(self): ) # examples being overriden by slot_usage modification - for example in cls.attributes["tempo"].examples: - self.assertIn(example.value, ["120.0", "144.0", "126.8", "102.6"]) + assert cls.attributes["tempo"].examples == [Example(value='120.0'), Example(value='144.0'), Example(value='126.8'), Example(value='102.6')] # annotations being overriden by slot_usage modification self.assertEqual( cls.attributes["tempo"].annotations.expected_value.value, From 15c17895d4ef776ad272a79549c998571582b87c Mon Sep 17 00:00:00 2001 From: Sujay Patil Date: Thu, 1 Aug 2024 14:58:25 -0700 Subject: [PATCH 3/6] address PR review comments on improving test_materialize_nonscalar_slot_usage() --- linkml_runtime/utils/schemaview.py | 6 ++- tests/test_issues/test_issue_2224.py | 51 ------------------- .../input/DJ_controller_schema.yaml} | 3 -- 3 files changed, 5 insertions(+), 55 deletions(-) delete mode 100644 tests/test_issues/test_issue_2224.py rename tests/{test_issues/input/linkml_issue_2224.yaml => test_utils/input/DJ_controller_schema.yaml} (93%) diff --git a/linkml_runtime/utils/schemaview.py b/linkml_runtime/utils/schemaview.py index 829729fc..60619281 100644 --- a/linkml_runtime/utils/schemaview.py +++ b/linkml_runtime/utils/schemaview.py @@ -1372,7 +1372,11 @@ def induced_slot(self, slot_name: SLOT_NAME, class_name: CLASS_NAME = None, impo else: # can rewrite below as: # 1. if v2: - # 2. if v2 is not None and ((isinstance(v2, (dict, list)) and v2) or (isinstance(v2, JsonObj) and as_dict(v2))) + # 2. if v2 is not None and + # ( + # (isinstance(v2, (dict, list)) and v2) or + # (isinstance(v2, JsonObj) and as_dict(v2)) + # ) if not is_empty(v2): v = v2 logging.debug(f'{v} takes precedence over {v2} for {induced_slot.name}.{metaslot_name}') diff --git a/tests/test_issues/test_issue_2224.py b/tests/test_issues/test_issue_2224.py deleted file mode 100644 index eeb77fcf..00000000 --- a/tests/test_issues/test_issue_2224.py +++ /dev/null @@ -1,51 +0,0 @@ -import unittest -from unittest import TestCase -from linkml_runtime.utils.schemaview import SchemaView -from jsonasobj2 import JsonObj - -from tests.test_issues.environment import env - - -class Issue2224TestCase(TestCase): - env = env - - def test_issue_2224_slot_classes(self): - sv = SchemaView(env.input_path("linkml_issue_2224.yaml")) - cls = sv.induced_class("DJController") - - # jog_wheels is a slot asserted at the schema level - # check that the range (scalar value) is being materialized properly - self.assertEqual(cls.attributes["jog_wheels"].range, "integer") - # check that the examples (list) is being materialized properly - self.assertIsInstance(cls.attributes["jog_wheels"].examples, list) - for example in cls.attributes["jog_wheels"].examples: - self.assertEqual(example.value, "2") - for example in cls.attributes["volume_faders"].examples: - self.assertEqual(example.value, "4") - for example in cls.attributes["crossfaders"].examples: - self.assertEqual(example.value, "1") - # check that the annotations (dictionary) is being materialized properly - self.assertIsInstance(cls.attributes["jog_wheels"].annotations, JsonObj) - self.assertEqual( - cls.attributes["jog_wheels"].annotations.expected_value.value, - "an integer between 0 and 4", - ) - self.assertEqual( - cls.attributes["volume_faders"].annotations.expected_value.value, - "an integer between 0 and 8", - ) - - # examples being overriden by slot_usage modification - assert cls.attributes["tempo"].examples == [Example(value='120.0'), Example(value='144.0'), Example(value='126.8'), Example(value='102.6')] - # annotations being overriden by slot_usage modification - self.assertEqual( - cls.attributes["tempo"].annotations.expected_value.value, - "a number between 0 and 300", - ) - self.assertEqual( - cls.attributes["tempo"].annotations.preferred_unit.value, "BPM" - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_issues/input/linkml_issue_2224.yaml b/tests/test_utils/input/DJ_controller_schema.yaml similarity index 93% rename from tests/test_issues/input/linkml_issue_2224.yaml rename to tests/test_utils/input/DJ_controller_schema.yaml index 1adc807e..1c82c9a5 100644 --- a/tests/test_issues/input/linkml_issue_2224.yaml +++ b/tests/test_utils/input/DJ_controller_schema.yaml @@ -17,9 +17,6 @@ classes: - value: 144.0 - value: 126.8 - value: 102.6 - annotations: - expected_value: a number between 0 and 300 - preferred_unit: BPM slots: jog_wheels: description: The number of jog wheels on the DJ controller From 929e147b57c9d2c7bcc71cfefaebc8982d7942de Mon Sep 17 00:00:00 2001 From: Sujay Patil Date: Thu, 1 Aug 2024 14:59:34 -0700 Subject: [PATCH 4/6] push up test_materialize_nonscalar_slot_usage() --- tests/test_utils/test_schemaview.py | 32 ++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/test_utils/test_schemaview.py b/tests/test_utils/test_schemaview.py index 367fb842..7f093c6b 100644 --- a/tests/test_utils/test_schemaview.py +++ b/tests/test_utils/test_schemaview.py @@ -6,8 +6,10 @@ from typing import List from unittest import TestCase +from jsonasobj2 import JsonObj + from linkml_runtime.dumpers import yaml_dumper -from linkml_runtime.linkml_model.meta import SchemaDefinition, ClassDefinition, SlotDefinitionName, SlotDefinition, \ +from linkml_runtime.linkml_model.meta import Example, SchemaDefinition, ClassDefinition, SlotDefinitionName, SlotDefinition, \ ClassDefinitionName, Prefix from linkml_runtime.loaders.yaml_loader import YAMLLoader from linkml_runtime.utils.introspection import package_schemaview @@ -945,6 +947,34 @@ def test_is_inlined(self): actual_result = sv.is_inlined(slot) self.assertEqual(actual_result, expected_result) + def test_materialize_nonscalar_slot_usage(self): + schema_path = os.path.join(INPUT_DIR, "DJ_controller_schema.yaml") + sv = SchemaView(schema_path) + cls = sv.induced_class("DJController") + + # jog_wheels is a slot asserted at the schema level + # check that the range (scalar value) is being materialized properly + assert cls.attributes["jog_wheels"].range == "integer" + # check that the examples (list) is being materialized properly + assert isinstance(cls.attributes["jog_wheels"].examples, list) + for example in cls.attributes["jog_wheels"].examples: + assert example.value == "2" + for example in cls.attributes["volume_faders"].examples: + assert example.value == "4" + for example in cls.attributes["crossfaders"].examples: + assert example.value == "1" + # check that the annotations (dictionary) is being materialized properly + assert isinstance(cls.attributes["jog_wheels"].annotations, JsonObj) + assert cls.attributes["jog_wheels"].annotations.expected_value.value == "an integer between 0 and 4" + assert cls.attributes["volume_faders"].annotations.expected_value.value == "an integer between 0 and 8" + + # examples being overridden by slot_usage modification + assert cls.attributes["tempo"].examples == [Example(value='120.0'), Example(value='144.0'), Example(value='126.8'), Example(value='102.6')] + # annotations remain the same / propagated as is from schema-level + # definition of `tempo` slot + assert cls.attributes["tempo"].annotations.expected_value.value == "a number between 0 and 200" + assert cls.attributes["tempo"].annotations.preferred_unit.value == "BPM" + if __name__ == '__main__': unittest.main() From 0f263b9689ff5f6034ddf3bd08b7de7448b54c16 Mon Sep 17 00:00:00 2001 From: Sujay Patil Date: Thu, 1 Aug 2024 16:56:32 -0700 Subject: [PATCH 5/6] docstring for `test_materialize_nonscalar_slot_usage()` in `tests/test_utils/test_schemaview.py` Co-authored-by: Jonny Saunders --- tests/test_utils/test_schemaview.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_utils/test_schemaview.py b/tests/test_utils/test_schemaview.py index 7f093c6b..705d19e7 100644 --- a/tests/test_utils/test_schemaview.py +++ b/tests/test_utils/test_schemaview.py @@ -948,6 +948,14 @@ def test_is_inlined(self): self.assertEqual(actual_result, expected_result) def test_materialize_nonscalar_slot_usage(self): + """ + ``slot_usage`` overrides values in the base slot definition without + clobbering unrelated, nonscalar values. + + See: + - https://github.com/linkml/linkml/issues/2224 + - https://github.com/linkml/linkml-runtime/pull/335 + """ schema_path = os.path.join(INPUT_DIR, "DJ_controller_schema.yaml") sv = SchemaView(schema_path) cls = sv.induced_class("DJController") From ba677c6fb8576390cf6a508ed49e0329cd17dc48 Mon Sep 17 00:00:00 2001 From: Sujay Patil Date: Wed, 7 Aug 2024 17:57:24 -0700 Subject: [PATCH 6/6] assertion that ensure that domain_of is not populated in slot_usage --- tests/test_utils/test_schemaview.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_utils/test_schemaview.py b/tests/test_utils/test_schemaview.py index 705d19e7..8e059689 100644 --- a/tests/test_utils/test_schemaview.py +++ b/tests/test_utils/test_schemaview.py @@ -983,6 +983,11 @@ def test_materialize_nonscalar_slot_usage(self): assert cls.attributes["tempo"].annotations.expected_value.value == "a number between 0 and 200" assert cls.attributes["tempo"].annotations.preferred_unit.value == "BPM" + assert cls.attributes["tempo"].domain_of == ["DJController"] + # ensure that domain_of is not being populated in slot_usage + # test for https://github.com/linkml/linkml/pull/2262 from upstream linkml + assert cls.slot_usage["tempo"].domain_of == [] + if __name__ == '__main__': unittest.main()