Repository: linkml/linkml

Issue:
linkml-sqldb dump can't handle slots with enum ranges
```
Traceback (most recent call last):
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/bin/linkml-sqldb", line 8, in <module>
    sys.exit(main())
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/click/core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/linkml/utils/sqlutils.py", line 336, in dump
    endpoint.dump(obj)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/linkml/utils/sqlutils.py", line 142, in dump
    nu_obj = self.to_sqla(element)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/linkml/utils/sqlutils.py", line 189, in to_sqla
    v2 = self.to_sqla(v)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/linkml/utils/sqlutils.py", line 170, in to_sqla
    nu_obj = [self.to_sqla(x) for x in obj]
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/linkml/utils/sqlutils.py", line 170, in <listcomp>
    nu_obj = [self.to_sqla(x) for x in obj]
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/linkml/utils/sqlutils.py", line 189, in to_sqla
    v2 = self.to_sqla(v)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/linkml/utils/sqlutils.py", line 189, in to_sqla
    v2 = self.to_sqla(v)
  File "/Users/MAM/Library/Caches/pypoetry/virtualenvs/linkml-project-template-d6UeGt0V-py3.9/lib/python3.9/site-packages/linkml/utils/sqlutils.py", line 198, in to_sqla
    raise ValueError(f'Cannot find {typ.__name__} in {self.module}')
ValueError: Cannot find PermissibleValue in <module 'test'>
```

Comments:

Patch:
diff --git a/linkml/utils/sqlutils.py b/linkml/utils/sqlutils.py
index f79ceeaf..290c0807 100644
--- a/linkml/utils/sqlutils.py
+++ b/linkml/utils/sqlutils.py
@@ -9,9 +9,10 @@ import inspect
 
 import click
 from linkml_runtime import SchemaView
-from linkml_runtime.linkml_model import SchemaDefinition
+from linkml_runtime.linkml_model import SchemaDefinition, PermissibleValue
 import linkml_runtime.linkml_model.meta as metamodel
 from linkml_runtime.utils.compile_python import compile_python
+from linkml_runtime.utils.enumerations import EnumDefinitionImpl
 from linkml_runtime.utils.formatutils import underscore
 from linkml_runtime.utils.yamlutils import YAMLRoot
 from linkml_runtime.utils.introspection import package_schemaview
@@ -182,6 +183,10 @@ class SQLStore:
                 return nu_obj
             else:
                 return None
+        #elif isinstance(obj, PermissibleValue):
+        #    return str(obj.text)
+        elif isinstance(obj, EnumDefinitionImpl):
+            return str(obj)
         elif isinstance(obj, YAMLRoot):
             typ = type(obj)
             inst_args = {}
@@ -236,7 +241,9 @@ class SQLStore:
             for n, nu_typ in inspect.getmembers(self.native_module):
                 # TODO: make more efficient
                 if n == typ.__name__:
+                    #print(f'CREATING {nu_typ} FROM {inst_args}')
                     nu_obj = nu_typ(**inst_args)
+                    #print(f'CREATED {nu_obj}')
                     return nu_obj
             raise ValueError(f'Cannot find {typ.__name__} in {self.native_module}')
         else:


Test patch:
diff --git a/tests/test_data/input/personinfo.yaml b/tests/test_data/input/personinfo.yaml
index b732b5ee..16429479 100644
--- a/tests/test_data/input/personinfo.yaml
+++ b/tests/test_data/input/personinfo.yaml
@@ -232,6 +232,7 @@ slots:
     maximum_value: 999
   related_to:
   type:
+    range: FamilialRelationshipType
   street:
   city:
   mission_statement:
@@ -283,17 +284,17 @@ enums:
         meaning: famrel:01
   GenderType:
     permissible_values:
-      nonbinary man:
+      nonbinary_man:
         meaning: GSSO:009254
-      nonbinary woman:
+      nonbinary_woman:
         meaning: GSSO:009253
-      transgender woman:
+      transgender_woman:
         meaning: GSSO:000384
-      transgender man:
+      transgender_man:
         meaning: GSSO:000372
-      cisgender man:
+      cisgender_man:
         meaning: GSSO:000371
-      cisgender woman:
+      cisgender_woman:
         meaning: GSSO:000385
   DiagnosisType:
 
diff --git a/tests/test_data/input/personinfo_data01.yaml b/tests/test_data/input/personinfo_data01.yaml
index 8428e865..24fb18b9 100644
--- a/tests/test_data/input/personinfo_data01.yaml
+++ b/tests/test_data/input/personinfo_data01.yaml
@@ -28,6 +28,7 @@ persons:
           name: prescribe cough medicine
   - id: X:P2
     name: person2
+    gender: cisgender_man
     aliases:
       - p2a1
       - p2a2
diff --git a/tests/test_data/model/personinfo.py b/tests/test_data/model/personinfo.py
index 87628747..7256ea12 100644
--- a/tests/test_data/model/personinfo.py
+++ b/tests/test_data/model/personinfo.py
@@ -1,5 +1,5 @@
 # Auto generated from personinfo.yaml by pythongen.py version: 0.9.0
-# Generation date: 2022-02-21T16:17:22
+# Generation date: 2022-06-01T12:12:50
 # Schema: personinfo
 #
 # id: https://w3id.org/linkml/examples/personinfo
@@ -33,6 +33,8 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
 
 # Namespaces
 GSSO = CurieNamespace('GSSO', 'http://purl.obolibrary.org/obo/GSSO_')
+ONT = CurieNamespace('ONT', 'http://example.org/ont/')
+X = CurieNamespace('X', 'http://example.org/data/')
 FAMREL = CurieNamespace('famrel', 'https://example.org/FamilialRelations#')
 LINKML = CurieNamespace('linkml', 'https://w3id.org/linkml/')
 PERSONINFO = CurieNamespace('personinfo', 'https://w3id.org/linkml/examples/personinfo/')
@@ -40,7 +42,7 @@ PROV = CurieNamespace('prov', 'http://www.w3.org/ns/prov#')
 RDF = CurieNamespace('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')
 RDFS = CurieNamespace('rdfs', 'http://www.w3.org/2000/01/rdf-schema#')
 SCHEMA = CurieNamespace('schema', 'http://schema.org/')
-SKOS = CurieNamespace('skos', 'http://example.org/UNKNOWN/skos/')
+SKOS = CurieNamespace('skos', 'http://www.w3.org/2004/02/skos/core#')
 XSD = CurieNamespace('xsd', 'http://www.w3.org/2001/XMLSchema#')
 DEFAULT_ = PERSONINFO
 
@@ -76,6 +78,10 @@ class ProcedureConceptId(ConceptId):
     pass
 
 
+class BiologicalSpecimenId(NamedThingId):
+    pass
+
+
 @dataclass
 class NamedThing(YAMLRoot):
     """
@@ -426,7 +432,7 @@ class Relationship(YAMLRoot):
     started_at_time: Optional[Union[str, XSDDate]] = None
     ended_at_time: Optional[Union[str, XSDDate]] = None
     related_to: Optional[str] = None
-    type: Optional[str] = None
+    type: Optional[Union[str, "FamilialRelationshipType"]] = None
 
     def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
         if self.started_at_time is not None and not isinstance(self.started_at_time, XSDDate):
@@ -438,8 +444,8 @@ class Relationship(YAMLRoot):
         if self.related_to is not None and not isinstance(self.related_to, str):
             self.related_to = str(self.related_to)
 
-        if self.type is not None and not isinstance(self.type, str):
-            self.type = str(self.type)
+        if self.type is not None and not isinstance(self.type, FamilialRelationshipType):
+            self.type = FamilialRelationshipType(self.type)
 
         super().__post_init__(**kwargs)
 
@@ -550,6 +556,26 @@ class WithLocation(YAMLRoot):
         super().__post_init__(**kwargs)
 
 
+@dataclass
+class BiologicalSpecimen(NamedThing):
+    _inherited_slots: ClassVar[List[str]] = []
+
+    class_class_uri: ClassVar[URIRef] = PERSONINFO.BiologicalSpecimen
+    class_class_curie: ClassVar[str] = "personinfo:BiologicalSpecimen"
+    class_name: ClassVar[str] = "biological specimen"
+    class_model_uri: ClassVar[URIRef] = PERSONINFO.BiologicalSpecimen
+
+    id: Union[str, BiologicalSpecimenId] = None
+
+    def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
+        if self._is_empty(self.id):
+            self.MissingRequiredField("id")
+        if not isinstance(self.id, BiologicalSpecimenId):
+            self.id = BiologicalSpecimenId(self.id)
+
+        super().__post_init__(**kwargs)
+
+
 @dataclass
 class Container(YAMLRoot):
     _inherited_slots: ClassVar[List[str]] = []
@@ -593,31 +619,23 @@ class FamilialRelationshipType(EnumDefinitionImpl):
 
 class GenderType(EnumDefinitionImpl):
 
+    nonbinary_man = PermissibleValue(text="nonbinary_man",
+                                                 meaning=GSSO["009254"])
+    nonbinary_woman = PermissibleValue(text="nonbinary_woman",
+                                                     meaning=GSSO["009253"])
+    transgender_woman = PermissibleValue(text="transgender_woman",
+                                                         meaning=GSSO["000384"])
+    transgender_man = PermissibleValue(text="transgender_man",
+                                                     meaning=GSSO["000372"])
+    cisgender_man = PermissibleValue(text="cisgender_man",
+                                                 meaning=GSSO["000371"])
+    cisgender_woman = PermissibleValue(text="cisgender_woman",
+                                                     meaning=GSSO["000385"])
+
     _defn = EnumDefinition(
         name="GenderType",
     )
 
-    @classmethod
-    def _addvals(cls):
-        setattr(cls, "nonbinary man",
-                PermissibleValue(text="nonbinary man",
-                                 meaning=GSSO["009254"]) )
-        setattr(cls, "nonbinary woman",
-                PermissibleValue(text="nonbinary woman",
-                                 meaning=GSSO["009253"]) )
-        setattr(cls, "transgender woman",
-                PermissibleValue(text="transgender woman",
-                                 meaning=GSSO["000384"]) )
-        setattr(cls, "transgender man",
-                PermissibleValue(text="transgender man",
-                                 meaning=GSSO["000372"]) )
-        setattr(cls, "cisgender man",
-                PermissibleValue(text="cisgender man",
-                                 meaning=GSSO["000371"]) )
-        setattr(cls, "cisgender woman",
-                PermissibleValue(text="cisgender woman",
-                                 meaning=GSSO["000385"]) )
-
 class DiagnosisType(EnumDefinitionImpl):
 
     _defn = EnumDefinition(
@@ -680,7 +698,7 @@ slots.related_to = Slot(uri=PERSONINFO.related_to, name="related_to", curie=PERS
                    model_uri=PERSONINFO.related_to, domain=None, range=Optional[str])
 
 slots.type = Slot(uri=PERSONINFO.type, name="type", curie=PERSONINFO.curie('type'),
-                   model_uri=PERSONINFO.type, domain=None, range=Optional[str])
+                   model_uri=PERSONINFO.type, domain=None, range=Optional[Union[str, "FamilialRelationshipType"]])
 
 slots.street = Slot(uri=PERSONINFO.street, name="street", curie=PERSONINFO.curie('street'),
                    model_uri=PERSONINFO.street, domain=None, range=Optional[str])
diff --git a/tests/test_data/test_sqlite.py b/tests/test_data/test_sqlite.py
index 24897c2c..5fb1a9cc 100644
--- a/tests/test_data/test_sqlite.py
+++ b/tests/test_data/test_sqlite.py
@@ -11,7 +11,7 @@ from sqlalchemy.orm import sessionmaker
 from linkml.utils.schema_builder import SchemaBuilder
 from linkml.utils.sqlutils import SQLStore
 
-from tests.test_data.model.personinfo import Container, Person
+from tests.test_data.model.personinfo import Container, Person, FamilialRelationship, GenderType, FamilialRelationshipType
 import tests.test_data.model.personinfo
 from tests.test_data.environment import env
 from tests.utils.dict_comparator import compare_yaml, compare_objs
@@ -33,6 +33,17 @@ class SQLiteStoreTest(unittest.TestCase):
     - :meth:`SQLStore.load`
     """
 
+    def test_enums(self):
+        """
+        Tests that enum objects can be constructed inlined.
+
+        See https://github.com/linkml/linkml/issues/817
+        """
+        r = FamilialRelationship(type='SIBLING_OF', related_to='x')
+        p = Person(id='x', gender=GenderType(GenderType.cisgender_man))
+        self.assertEqual(type(p.gender), GenderType)
+        c = Container(persons=[p])
+
     def test_sqlite_store(self):
         """
         tests a complete end-to-end example with a dump-load cycle
@@ -54,6 +65,14 @@ class SQLiteStoreTest(unittest.TestCase):
         q = session.query(endpoint.module.Person)
         all_objs = q.all()
         self.assertEqual(2, len(all_objs))
+        for p in all_objs:
+            print(p)
+            for rel in p.has_familial_relationships:
+                print(rel)
+                print(rel.type)
+        q = session.query(endpoint.module.FamilialRelationship)
+        for r in q.all():
+            print(r)
         # step 4: test loading from SQLStore
         # 4a: first test load_all, diff to original data should be empty
         x = endpoint.load_all(target_class=Container)
@@ -80,8 +99,8 @@ class SQLiteStoreTest(unittest.TestCase):
         endpoint.compile()
         # step 2: load data from file and store in SQLStore
         container: SchemaDefinition = yaml_loader.load(SCHEMA, target_class=SchemaDefinition)
-        endpoint.dump(container)
-
+        schema_instance = SchemaDefinition(id='test', name='test')
+        endpoint.dump(schema_instance)
 
     def test_mixin(self):
         b = SchemaBuilder()