Repository: linkml/linkml

Issue:
ifabsent_processor does not handle default_range
---
name: Bug report
about: Create a report to help us improve
title: 'ifabsent_processor does not handle default_range'
labels: bug
assignees: ''

---

**Describe the bug**

One of the permissible values for `ifabsent` is the function `default_range`.

There is exactly one use of this:

```yaml
  range:
    aliases:
      - value domain
    domain: slot_definition
    range: element
    ifabsent: default_range
```

This is not currently handled by the ifabsent_processor:

`poetry run gen-shacl --stacktrace  linkml_model/model/schema/meta.yaml`

```
Traceback (most recent call last):
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/bin/gen-shacl", line 8, in <module>
    sys.exit(cli())
             ^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/click/core.py", line 1161, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/click/core.py", line 1082, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/click/core.py", line 1443, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/click/core.py", line 788, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/linkml/generators/shaclgen.py", line 371, in cli
    print(gen.serialize())
          ^^^^^^^^^^^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/linkml/generators/shaclgen.py", line 52, in serialize
    g = self.as_graph()
        ^^^^^^^^^^^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/linkml/generators/shaclgen.py", line 221, in as_graph
    default_value = ifabsent_processor.process_slot(s, c)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/linkml/generators/common/ifabsent_processor.py", line 87, in process_slot
    return self._map_to_default_value(slot, ifabsent_default_value, cls)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/cjm/Library/Caches/pypoetry/virtualenvs/linkml-model-B15i4R6l-py3.11/lib/python3.11/site-packages/linkml/generators/common/ifabsent_processor.py", line 211, in _map_to_default_value
    raise ValueError(f"The ifabsent value `{slot.ifabsent}` of the `{slot.name}` slot could not be processed")
ValueError: The ifabsent value `default_range` of the `range` slot could not be processed
```

Note that I don't think there is a need to actively do anything here

**Version of LinkML you are using**

1.8.6

**Please provide a schema (and if applicable, a data file) that replicates the issue**
You can include this in th issue if small, or provide a link e.g. to github.
Ideally a minimal example that replicates the problem is ideal, but only if this is easy for you.

Discussion hints:

Solution patch:
diff --git a/packages/linkml/src/linkml/generators/shacl/shacl_ifabsent_processor.py b/packages/linkml/src/linkml/generators/shacl/shacl_ifabsent_processor.py
index f13923c13..d18224138 100644
--- a/packages/linkml/src/linkml/generators/shacl/shacl_ifabsent_processor.py
+++ b/packages/linkml/src/linkml/generators/shacl/shacl_ifabsent_processor.py
@@ -13,6 +13,11 @@ def map_custom_default_values(self, default_value: str, slot: SlotDefinition, cl
                 return True, URIRef(class_uri)
             return True, ""
 
+        # TODO: The below conditional is a stop gap for a proper solution, see @sneakers-the-rat proposal in https://github.com/linkml/linkml/issues/2522
+        if default_value == "default_range":
+            default_range = self.schema_view.schema.default_range or "string"
+            return True, Literal(default_range, datatype=ShaclDataType.STRING.uri_ref)
+
         return False, None
 
     def map_enum_default_value(


Test patch:
diff --git a/tests/linkml/test_compliance/test_ifabsent_compliance.py b/tests/linkml/test_compliance/test_ifabsent_compliance.py
index 4b93e3ce2..ea2e2f05f 100644
--- a/tests/linkml/test_compliance/test_ifabsent_compliance.py
+++ b/tests/linkml/test_compliance/test_ifabsent_compliance.py
@@ -39,6 +39,7 @@
         ("boolT", "boolean", "true", "no_value", None, True, True, True, []),
         ("boolF", "boolean", "false", "no_value", None, False, True, True, []),
         ("class_curie", "uriorcurie", "class_curie", "no_value", None, "ex:C", True, True, []),
+        ("default_range", "string", "default_range", "no_value", None, "string", True, True, []),
         ("D", CLASS_D, "string(p1)", "no_value", None, "p1", False, True, []),
         # Skip Python, Pydantic and Shacl frameworks because this incompatibility is not possible with the processor
         (
diff --git a/tests/linkml/test_generators/shacl/test_shacl_ifabsent_processor.py b/tests/linkml/test_generators/shacl/test_shacl_ifabsent_processor.py
index 782525501..0b77f9639 100644
--- a/tests/linkml/test_generators/shacl/test_shacl_ifabsent_processor.py
+++ b/tests/linkml/test_generators/shacl/test_shacl_ifabsent_processor.py
@@ -482,3 +482,35 @@ def test_process_sparql_path_ifabsent_attribute():
             schema_view.all_slots()[SlotDefinitionName("unimplementedSparqlPath")],
             schema_view.all_classes()[ClassDefinitionName("Student")],
         )
+
+
+def test_process_default_range():
+    """Test that default_range resolves to the schema's default range value."""
+    schema_with_default_range = """
+id: ifabsent_tests
+name: ifabsent_tests
+default_range: string
+
+prefixes:
+  ex: https://example.org/
+default_prefix: ex
+
+classes:
+  Student:
+    attributes:
+"""
+    schema_view = SchemaView(
+        schema_with_default_range
+        + """
+    - name: rangeSlot
+      range: string
+      ifabsent: default_range
+    """
+    )
+
+    processor = ShaclIfAbsentProcessor(schema_view)
+
+    assert processor.process_slot(
+        schema_view.all_slots()[SlotDefinitionName("rangeSlot")],
+        schema_view.all_classes()[ClassDefinitionName("Student")],
+    ) == Literal("string", datatype=ShaclDataType.STRING.uri_ref)
diff --git a/tests/linkml/test_metamodel_compat/test_metamodel_projectgen.py b/tests/linkml/test_metamodel_compat/test_metamodel_projectgen.py
index 320754a04..9d9ee548d 100644
--- a/tests/linkml/test_metamodel_compat/test_metamodel_projectgen.py
+++ b/tests/linkml/test_metamodel_compat/test_metamodel_projectgen.py
@@ -36,7 +36,7 @@ def test_metamodel_python_compiles(metamodel_path, tmp_path):
     config = ProjectConfiguration()
     config.directory = tmp_path
     config.generator_args["owl"] = {"metaclasses": False, "type_objects": False}
-    config.excludes = ["shacl", "excel"]
+    config.excludes = ["excel"]
     gen = ProjectGenerator()
     gen.generate(metamodel_path, config)
     python_file = tmp_path / "meta.py"