From 7ccc64911126e09520aec9a763940da2b2ccba32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 31 Oct 2026 02:37:54 +0100 Subject: [PATCH 1/2] Add unfoldable, fixes #470 --- terminusdb_client/schema/schema.py | 6 +++ terminusdb_client/scripts/scripts.py | 5 +++ terminusdb_client/tests/test_Schema.py | 42 +++++++++++++++++++ .../tests/test_schema_overall.py | 11 +++++ terminusdb_client/tests/test_scripts.py | 12 ++++++ 5 files changed, 76 insertions(+) diff --git a/terminusdb_client/schema/schema.py b/terminusdb_client/schema/schema.py index 78ec445b..d1fb36c9 100644 --- a/terminusdb_client/schema/schema.py +++ b/terminusdb_client/schema/schema.py @@ -179,6 +179,8 @@ def __init__(cls, name, bases, nmspc): # _abstract should not be inherited cls._abstract = nmspc.get("_abstract") + # _unfoldable should not be inherited + cls._unfoldable = nmspc.get("_unfoldable") cls._instances = set() def init(obj, *args, **kwargs): @@ -288,6 +290,8 @@ def _to_dict(cls, skip_checking=False): result["@key"] = {"@type": "Random"} if hasattr(cls, "_abstract") and cls._abstract is not None: result["@abstract"] = cls._abstract + if hasattr(cls, "_unfoldable") and cls._unfoldable is not None: + result["@unfoldable"] = cls._unfoldable if hasattr(cls, "_key") and not hasattr(cls, "_subdocument"): if hasattr(cls._key, "_keys"): result["@key"] = { @@ -591,6 +595,8 @@ def _construct_class(self, class_obj_dict): attributedict["_subdocument"] = value elif key == "@abstract": attributedict["_abstract"] = value + elif key == "@unfoldable": + attributedict["_unfoldable"] = value elif key == "@key": key_type = value.get("@type") if key_type and key_type == "Random": diff --git a/terminusdb_client/scripts/scripts.py b/terminusdb_client/scripts/scripts.py index ffb4b365..24a571d9 100644 --- a/terminusdb_client/scripts/scripts.py +++ b/terminusdb_client/scripts/scripts.py @@ -227,6 +227,9 @@ def add_subdoc(self): def add_abstract(self): self.script += " _abstract = []\n" + def add_unfoldable(self): + self.script += " _unfoldable = []\n" + def add_docstring(self, obj_dict): self.script += ' """' if obj_dict["@documentation"].get("@comment"): @@ -297,6 +300,8 @@ def add_docstring(self, obj_dict): if obj.get("@abstract") is not None: result_obj.add_abstract() + if obj.get("@unfoldable") is not None: + result_obj.add_unfoldable() result_list.append(result_obj) for key, value in obj.items(): diff --git a/terminusdb_client/tests/test_Schema.py b/terminusdb_client/tests/test_Schema.py index df2d19f4..85a07874 100644 --- a/terminusdb_client/tests/test_Schema.py +++ b/terminusdb_client/tests/test_Schema.py @@ -35,6 +35,20 @@ class ChildAbs(Abstract): # _abstract = False +class Unfoldable(DocumentTemplate): + """This is unfoldable class""" + + _schema = Schema() + _unfoldable = [] + name: str + + +class ChildUnf(Unfoldable): + """Child of unfoldable class, should not be unfoldable by default""" + + # _unfoldable = False + + class TypeCheck(DocumentTemplate): """For checking instance types""" @@ -112,6 +126,34 @@ def test_abstract_class_child(): assert "@abstract" not in ChildAbs._to_dict() +def test_unfoldable_class(): + it_ok_to_create = Unfoldable(name="It's ok") + assert it_ok_to_create.name == "It's ok" + assert Unfoldable._unfoldable == [] + assert Unfoldable._to_dict().get("@unfoldable") == [] + + +def test_unfoldable_class_child(): + it_ok_to_create = ChildUnf(name="It's ok") + assert it_ok_to_create.name == "It's ok" + assert ChildUnf._unfoldable is None + assert "@unfoldable" not in ChildUnf._to_dict() + + +def test_construct_class_with_unfoldable(): + schema_json = { + "@id": "UnfoldableDoc", + "@type": "Class", + "@unfoldable": [], + "@key": {"@type": "Random"}, + "name": "xsd:string", + } + schema = Schema() + test_result = schema._construct_class(schema_json) + assert test_result._unfoldable == [] + assert test_result._to_dict() == schema_json + + @pytest.mark.skip(reason="relaxing type checking") def test_type_check(): test_obj = TypeCheck() diff --git a/terminusdb_client/tests/test_schema_overall.py b/terminusdb_client/tests/test_schema_overall.py index b2db6303..583210f9 100644 --- a/terminusdb_client/tests/test_schema_overall.py +++ b/terminusdb_client/tests/test_schema_overall.py @@ -404,6 +404,17 @@ class Doc(DocumentTemplate): assert result.get("@abstract") is True + def test_to_dict_with_unfoldable(self): + """Test _to_dict with _unfoldable list""" + + class Doc(DocumentTemplate): + name: str + _unfoldable = [] + + result = Doc._to_dict() + + assert result.get("@unfoldable") == [] + def test_to_dict_with_hashkey(self): """Test _to_dict with HashKey""" diff --git a/terminusdb_client/tests/test_scripts.py b/terminusdb_client/tests/test_scripts.py index 3afcf6c8..e2a7401b 100644 --- a/terminusdb_client/tests/test_scripts.py +++ b/terminusdb_client/tests/test_scripts.py @@ -382,6 +382,18 @@ def test_create_script_parent_string(): assert "class Child2(Parent1):" in result +def test_create_script_unfoldable(): + """Test _create_script emitting _unfoldable for @unfoldable classes""" + obj_list = [ + {"@documentation": {"@title": "Test Schema"}}, + {"@id": "UnfoldableDoc", "@type": "Class", "@unfoldable": []}, + ] + + result = scripts._create_script(obj_list) + assert "class UnfoldableDoc(DocumentTemplate):" in result + assert "_unfoldable = []" in result + + def test_sync_empty_schema(): """Test _sync with empty schema""" mock_client = MagicMock() From 4fb425265c980e8cb501be1ef25a2fc3f1e8dd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Sat, 31 Oct 2026 02:39:33 +0100 Subject: [PATCH 2/2] Also add support for @shared --- terminusdb_client/schema/schema.py | 6 +++ terminusdb_client/scripts/scripts.py | 5 ++ terminusdb_client/tests/test_Schema.py | 46 +++++++++++++++++++ .../tests/test_schema_overall.py | 11 +++++ terminusdb_client/tests/test_scripts.py | 12 +++++ 5 files changed, 80 insertions(+) diff --git a/terminusdb_client/schema/schema.py b/terminusdb_client/schema/schema.py index d1fb36c9..2f0bea08 100644 --- a/terminusdb_client/schema/schema.py +++ b/terminusdb_client/schema/schema.py @@ -181,6 +181,8 @@ def __init__(cls, name, bases, nmspc): cls._abstract = nmspc.get("_abstract") # _unfoldable should not be inherited cls._unfoldable = nmspc.get("_unfoldable") + # _shared should not be inherited + cls._shared = nmspc.get("_shared") cls._instances = set() def init(obj, *args, **kwargs): @@ -292,6 +294,8 @@ def _to_dict(cls, skip_checking=False): result["@abstract"] = cls._abstract if hasattr(cls, "_unfoldable") and cls._unfoldable is not None: result["@unfoldable"] = cls._unfoldable + if hasattr(cls, "_shared") and cls._shared is not None: + result["@shared"] = cls._shared if hasattr(cls, "_key") and not hasattr(cls, "_subdocument"): if hasattr(cls._key, "_keys"): result["@key"] = { @@ -597,6 +601,8 @@ def _construct_class(self, class_obj_dict): attributedict["_abstract"] = value elif key == "@unfoldable": attributedict["_unfoldable"] = value + elif key == "@shared": + attributedict["_shared"] = value elif key == "@key": key_type = value.get("@type") if key_type and key_type == "Random": diff --git a/terminusdb_client/scripts/scripts.py b/terminusdb_client/scripts/scripts.py index 24a571d9..ec275af6 100644 --- a/terminusdb_client/scripts/scripts.py +++ b/terminusdb_client/scripts/scripts.py @@ -230,6 +230,9 @@ def add_abstract(self): def add_unfoldable(self): self.script += " _unfoldable = []\n" + def add_shared(self): + self.script += " _shared = []\n" + def add_docstring(self, obj_dict): self.script += ' """' if obj_dict["@documentation"].get("@comment"): @@ -302,6 +305,8 @@ def add_docstring(self, obj_dict): result_obj.add_abstract() if obj.get("@unfoldable") is not None: result_obj.add_unfoldable() + if obj.get("@shared") is not None: + result_obj.add_shared() result_list.append(result_obj) for key, value in obj.items(): diff --git a/terminusdb_client/tests/test_Schema.py b/terminusdb_client/tests/test_Schema.py index 85a07874..1fa5eaf3 100644 --- a/terminusdb_client/tests/test_Schema.py +++ b/terminusdb_client/tests/test_Schema.py @@ -49,6 +49,20 @@ class ChildUnf(Unfoldable): # _unfoldable = False +class Shared(DocumentTemplate): + """This is shared class""" + + _schema = Schema() + _shared = [] + name: str + + +class ChildShared(Shared): + """Child of shared class, should not be shared by default""" + + # _shared = False + + class TypeCheck(DocumentTemplate): """For checking instance types""" @@ -154,6 +168,38 @@ def test_construct_class_with_unfoldable(): assert test_result._to_dict() == schema_json +def test_shared_class(): + it_ok_to_create = Shared(name="It's ok") + assert it_ok_to_create.name == "It's ok" + assert Shared._shared == [] + class_dict = Shared._to_dict() + assert class_dict.get("@shared") == [] + # shared documents are regular documents: they keep their key + assert class_dict.get("@key") == {"@type": "Random"} + assert "@subdocument" not in class_dict + + +def test_shared_class_child(): + it_ok_to_create = ChildShared(name="It's ok") + assert it_ok_to_create.name == "It's ok" + assert ChildShared._shared is None + assert "@shared" not in ChildShared._to_dict() + + +def test_construct_class_with_shared(): + schema_json = { + "@id": "SharedDoc", + "@type": "Class", + "@shared": [], + "@key": {"@type": "Random"}, + "name": "xsd:string", + } + schema = Schema() + test_result = schema._construct_class(schema_json) + assert test_result._shared == [] + assert test_result._to_dict() == schema_json + + @pytest.mark.skip(reason="relaxing type checking") def test_type_check(): test_obj = TypeCheck() diff --git a/terminusdb_client/tests/test_schema_overall.py b/terminusdb_client/tests/test_schema_overall.py index 583210f9..8b22da68 100644 --- a/terminusdb_client/tests/test_schema_overall.py +++ b/terminusdb_client/tests/test_schema_overall.py @@ -415,6 +415,17 @@ class Doc(DocumentTemplate): assert result.get("@unfoldable") == [] + def test_to_dict_with_shared(self): + """Test _to_dict with _shared list""" + + class Doc(DocumentTemplate): + name: str + _shared = [] + + result = Doc._to_dict() + + assert result.get("@shared") == [] + def test_to_dict_with_hashkey(self): """Test _to_dict with HashKey""" diff --git a/terminusdb_client/tests/test_scripts.py b/terminusdb_client/tests/test_scripts.py index e2a7401b..126354cc 100644 --- a/terminusdb_client/tests/test_scripts.py +++ b/terminusdb_client/tests/test_scripts.py @@ -394,6 +394,18 @@ def test_create_script_unfoldable(): assert "_unfoldable = []" in result +def test_create_script_shared(): + """Test _create_script emitting _shared for @shared classes""" + obj_list = [ + {"@documentation": {"@title": "Test Schema"}}, + {"@id": "SharedDoc", "@type": "Class", "@shared": []}, + ] + + result = scripts._create_script(obj_list) + assert "class SharedDoc(DocumentTemplate):" in result + assert "_shared = []" in result + + def test_sync_empty_schema(): """Test _sync with empty schema""" mock_client = MagicMock()