From ddf7d2e27c341955d832ad4891a5f827fb4d2034 Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Sat, 19 Oct 2024 23:36:58 +0100 Subject: [PATCH 1/2] Make BaseObject the parent class for Date, StyledText and StyledTextTag --- gramps/gen/lib/date.py | 3 ++- gramps/gen/lib/styledtext.py | 3 ++- gramps/gen/lib/styledtexttag.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/gramps/gen/lib/date.py b/gramps/gen/lib/date.py index e7e957c81b3..8905966792e 100644 --- a/gramps/gen/lib/date.py +++ b/gramps/gen/lib/date.py @@ -38,6 +38,7 @@ # Gramps modules # # ------------------------------------------------------------------------ +from .baseobj import BaseObject from ..config import config from ..const import GRAMPS_LOCALE as glocale from ..errors import DateError @@ -563,7 +564,7 @@ def _diff(self, date1, date2): # Date # # ------------------------------------------------------------------------- -class Date: +class Date(BaseObject): """ The core date handling class for Gramps. diff --git a/gramps/gen/lib/styledtext.py b/gramps/gen/lib/styledtext.py index c64ed768055..1c793115a16 100644 --- a/gramps/gen/lib/styledtext.py +++ b/gramps/gen/lib/styledtext.py @@ -35,6 +35,7 @@ # # ------------------------------------------------------------------------- from ..const import GRAMPS_LOCALE as glocale +from .baseobj import BaseObject from .styledtexttag import StyledTextTag _ = glocale.translation.gettext @@ -45,7 +46,7 @@ # StyledText # # ------------------------------------------------------------------------- -class StyledText: +class StyledText(BaseObject): """Helper class to enable character based text formatting. :py:class:`StyledText` is a wrapper class binding the clear text string and diff --git a/gramps/gen/lib/styledtexttag.py b/gramps/gen/lib/styledtexttag.py index dfe3c716f43..e1ee33396cc 100644 --- a/gramps/gen/lib/styledtexttag.py +++ b/gramps/gen/lib/styledtexttag.py @@ -28,6 +28,7 @@ # # ------------------------------------------------------------------------- from ..const import GRAMPS_LOCALE as glocale +from .baseobj import BaseObject from .styledtexttagtype import StyledTextTagType _ = glocale.translation.gettext @@ -38,7 +39,7 @@ # StyledTextTag # # ------------------------------------------------------------------------- -class StyledTextTag: +class StyledTextTag(BaseObject): """Hold formatting information for :py:class:`.StyledText`. :py:class:`StyledTextTag` is a container class, it's attributes are From 1dd0a91cd3abb777e2105b7e5c7be1b4b2f46191 Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Tue, 15 Oct 2024 00:27:02 +0100 Subject: [PATCH 2/2] Refactor JSON serialization --- gramps/gen/lib/baseobj.py | 43 +++++++++++++++++++++++++++ gramps/gen/lib/date.py | 24 ++++++++++++++- gramps/gen/lib/event.py | 24 ++++++++++++++- gramps/gen/lib/eventref.py | 22 +++++++++++++- gramps/gen/lib/grampstype.py | 29 ++++++++++++++++-- gramps/gen/lib/mediaref.py | 14 ++++++++- gramps/gen/lib/person.py | 28 ++++++++++++++--- gramps/gen/lib/serialize.py | 38 ++++++----------------- gramps/gen/lib/styledtext.py | 23 +++++++++++++- gramps/gen/lib/styledtexttag.py | 16 ++++++++-- gramps/gen/lib/tag.py | 30 +++++++++++++++++-- gramps/gen/lib/test/serialize_test.py | 5 ---- 12 files changed, 246 insertions(+), 50 deletions(-) diff --git a/gramps/gen/lib/baseobj.py b/gramps/gen/lib/baseobj.py index f5bf2e6a0cd..0a10bd72094 100644 --- a/gramps/gen/lib/baseobj.py +++ b/gramps/gen/lib/baseobj.py @@ -2,6 +2,7 @@ # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2000-2006 Donald N. Allingham +# Copyright (C) 2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -57,6 +58,48 @@ def unserialize(self, data): Convert a serialized tuple of data to an object. """ + def get_object_state(self): + """ + Get the current object state as a dictionary. + + By default this returns the public attributes of the instance. This + method can be overridden if the class requires other attributes or + properties to be saved. + + This method is called to provide the information required to serialize + the object. If None is returned then the object will be represented as + null in JSON. + + :returns: Returns a dictionary of attributes that represent the state + of the object or None. + :rtype: dict + """ + attr_dict = dict( + (key, value) + for key, value in self.__dict__.items() + if not key.startswith("_") + ) + attr_dict["_class"] = self.__class__.__name__ + return attr_dict + + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + By default this sets the state of the object assuming that all items in + the dictionary map to public attributes. This method can be overridden + to set the state using custom functionality. For performance reasons + it is useful to set a property without calling its setter function. As + JSON provides no distinction between tuples and lists, this method can + also be use to convert lists into tuples where required. + + :param attr_dict: A dictionary of attributes that represent the state of + the object. + :type attr_dict: dict + """ + self.__dict__.update(attr_dict) + def matches_string(self, pattern, case_sensitive=False): """ Return True if any text data in the object or any of it's child diff --git a/gramps/gen/lib/date.py b/gramps/gen/lib/date.py index 8905966792e..34c5a39eba6 100644 --- a/gramps/gen/lib/date.py +++ b/gramps/gen/lib/date.py @@ -5,7 +5,7 @@ # Copyright (C) 2009-2013 Douglas S. Blank # Copyright (C) 2013 Paul Franklin # Copyright (C) 2013-2014 Vassilii Khachaturov -# Copyright (C) 2017 Nick Hall +# Copyright (C) 2017,2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -750,6 +750,28 @@ def unserialize(self, data): raise DateError("Invalid date to unserialize") return self + def get_object_state(self): + """ + Get the current object state as a dictionary. + + We override this method to represent an empty date as null in JSON. + """ + if self.is_empty() and not self.text: + return None + else: + return super().get_object_state() + + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to convert `dateval` into a tuple. + """ + if "dateval" in attr_dict: + attr_dict["dateval"] = tuple(attr_dict["dateval"]) + super().set_object_state(attr_dict) + @classmethod def get_schema(cls): """ diff --git a/gramps/gen/lib/event.py b/gramps/gen/lib/event.py index 2fc1980b027..464b3a297b2 100644 --- a/gramps/gen/lib/event.py +++ b/gramps/gen/lib/event.py @@ -4,7 +4,7 @@ # Copyright (C) 2000-2007 Donald N. Allingham # Copyright (C) 2010 Michiel D. Nauta # Copyright (C) 2011 Tim G L Lyons -# Copyright (C) 2017 Nick Hall +# Copyright (C) 2017,2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -136,6 +136,28 @@ def serialize(self, no_text_date=False): self.private, ) + def get_object_state(self): + """ + Get the current object state as a dictionary. + + We override this method to handle the `type` and `description` properties. + """ + attr_dict = super().get_object_state() + attr_dict["type"] = self.__type + attr_dict["description"] = self.__description + return attr_dict + + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to handle the `type` and `description` properties. + """ + self.__type = attr_dict.pop("type") + self.__description = attr_dict.pop("description") + super().set_object_state(attr_dict) + @classmethod def get_schema(cls): """ diff --git a/gramps/gen/lib/eventref.py b/gramps/gen/lib/eventref.py index 655cc6be7aa..8898d0f9a86 100644 --- a/gramps/gen/lib/eventref.py +++ b/gramps/gen/lib/eventref.py @@ -5,7 +5,7 @@ # Copyright (C) 2010 Michiel D. Nauta # Copyright (C) 2011 Tim G L Lyons # Copyright (C) 2013 Doug Blank -# Copyright (C) 2017 Nick Hall +# Copyright (C) 2017,2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -86,6 +86,26 @@ def serialize(self): self.__role.serialize(), ) + def get_object_state(self): + """ + Get the current object state as a dictionary. + + We override this method to handle the `role` property. + """ + attr_dict = super().get_object_state() + attr_dict["role"] = self.__role + return attr_dict + + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to handle the `role` property. + """ + self.__role = attr_dict.pop("role") + super().set_object_state(attr_dict) + @classmethod def get_schema(cls): """ diff --git a/gramps/gen/lib/grampstype.py b/gramps/gen/lib/grampstype.py index 8e9d94480b6..39818ea8774 100644 --- a/gramps/gen/lib/grampstype.py +++ b/gramps/gen/lib/grampstype.py @@ -2,7 +2,7 @@ # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2000-2007 Donald N. Allingham -# Copyright (C) 2017 Nick Hall +# Copyright (C) 2017,2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -136,6 +136,30 @@ def __init__(self, value=None): if value is not None: self.set(value) + def get_object_state(self): + """ + Get the current object state as a dictionary. + + We override this method to handle the `value` and `string` properties. + """ + attr_dict = {"_class": self.__class__.__name__} + attr_dict["value"] = self.__value + attr_dict["string"] = self.__string + return attr_dict + + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to handle the `value` and `string` properties. + """ + self.__value = attr_dict["value"] + if self.__value == self._CUSTOM: + self.__string = attr_dict["string"] + else: + self.__string = "" + def __set_tuple(self, value): "Set the value/string properties from a tuple." val, strg = self._DEFAULT, "" @@ -225,7 +249,8 @@ def get_schema(cls): "title": _("Type"), "properties": { "_class": {"enum": [cls.__name__]}, - "string": {"type": "string", "title": _("Type")}, + "string": {"type": "string", "title": _("Custom type")}, + "value": {"type": "integer", "title": _("Type code")}, }, } diff --git a/gramps/gen/lib/mediaref.py b/gramps/gen/lib/mediaref.py index 9e52e116080..04e08463672 100644 --- a/gramps/gen/lib/mediaref.py +++ b/gramps/gen/lib/mediaref.py @@ -5,7 +5,7 @@ # Copyright (C) 2010 Michiel D. Nauta # Copyright (C) 2011 Tim G L Lyons # Copyright (C) 2013 Doug Blank -# Copyright (C) 2017 Nick Hall +# Copyright (C) 2017,2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -151,6 +151,18 @@ def unserialize(self, data): RefBase.unserialize(self, ref) return self + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to convert `rect` into a tuple. + """ + rect = attr_dict["rect"] + if rect is not None: + attr_dict["rect"] = tuple(rect) + super().set_object_state(attr_dict) + def get_text_data_child_list(self): """ Return the list of child objects that may carry textual data. diff --git a/gramps/gen/lib/person.py b/gramps/gen/lib/person.py index 1d59671fe86..dc45567acea 100644 --- a/gramps/gen/lib/person.py +++ b/gramps/gen/lib/person.py @@ -1,10 +1,10 @@ # # Gramps - a GTK+/GNOME based genealogy program # -# Copyright (C) 2000-2007 Donald N. Allingham -# Copyright (C) 2010 Michiel D. Nauta -# Copyright (C) 2010,2017 Nick Hall -# Copyright (C) 2011 Tim G L Lyons +# Copyright (C) 2000-2007 Donald N. Allingham +# Copyright (C) 2010 Michiel D. Nauta +# Copyright (C) 2010,2017,2024 Nick Hall +# Copyright (C) 2011 Tim G L Lyons # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -322,6 +322,26 @@ def unserialize(self, data): TagBase.unserialize(self, tag_list) return self + def get_object_state(self): + """ + Get the current object state as a dictionary. + + We override this method to handle the `gender` property. + """ + attr_dict = super().get_object_state() + attr_dict["gender"] = self.__gender + return attr_dict + + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to handle the `gender` property. + """ + self.__gender = attr_dict.pop("gender") + super().set_object_state(attr_dict) + def _has_handle_reference(self, classname, handle): """ Return True if the object has reference to a given handle of given diff --git a/gramps/gen/lib/serialize.py b/gramps/gen/lib/serialize.py index df0c323bd7a..2a1d863bf1e 100644 --- a/gramps/gen/lib/serialize.py +++ b/gramps/gen/lib/serialize.py @@ -1,7 +1,7 @@ # # Gramps - a GTK+/GNOME based genealogy program # -# Copyright (C) 2017 Nick Hall +# Copyright (C) 2017,2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,38 +37,18 @@ import gramps.gen.lib as lib -def __default(obj): - obj_dict = {"_class": obj.__class__.__name__} - if isinstance(obj, lib.GrampsType): - obj_dict["string"] = getattr(obj, "string") - if isinstance(obj, lib.Date): - if obj.is_empty() and not obj.text: - return None - for key, value in obj.__dict__.items(): - if not key.startswith("_"): - obj_dict[key] = value - for key, value in obj.__class__.__dict__.items(): - if isinstance(value, property): - if key != "year": - obj_dict[key] = getattr(obj, key) - return obj_dict - - def __object_hook(obj_dict): - obj = getattr(lib, obj_dict["_class"])() - for key, value in obj_dict.items(): - if key != "_class": - if key in ("dateval", "rect") and value is not None: - value = tuple(value) - if key == "ranges": - value = [tuple(item) for item in value] - setattr(obj, key, value) - if obj_dict["_class"] == "Date": - if obj.is_empty() and not obj.text: - return None + _class = obj_dict.pop("_class") + cls = lib.__dict__[_class] + obj = cls.__new__(cls) + obj.set_object_state(obj_dict) return obj +def __default(obj): + return obj.get_object_state() + + def to_json(obj): """ Encode a Gramps object in JSON format. diff --git a/gramps/gen/lib/styledtext.py b/gramps/gen/lib/styledtext.py index 1c793115a16..8f358eac0d5 100644 --- a/gramps/gen/lib/styledtext.py +++ b/gramps/gen/lib/styledtext.py @@ -3,7 +3,7 @@ # # Copyright (C) 2008 Zsolt Foldvari # Copyright (C) 2013 Doug Blank -# Copyright (C) 2017 Nick Hall +# Copyright (C) 2017,2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -102,6 +102,27 @@ def __init__(self, text="", tags=None): else: self._tags = [] + def get_object_state(self): + """ + Get the current object state as a dictionary. + + We override this method to handle the `tags` and `string` properties. + """ + attr_dict = {"_class": self.__class__.__name__} + attr_dict["tags"] = self._tags + attr_dict["string"] = self._string + return attr_dict + + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to handle the `tags` and `string` properties. + """ + self._tags = attr_dict["tags"] + self._string = attr_dict["string"] + # special methods def __str__(self): diff --git a/gramps/gen/lib/styledtexttag.py b/gramps/gen/lib/styledtexttag.py index e1ee33396cc..aaff72c7a20 100644 --- a/gramps/gen/lib/styledtexttag.py +++ b/gramps/gen/lib/styledtexttag.py @@ -1,9 +1,9 @@ # # Gramps - a GTK+/GNOME based genealogy program # -# Copyright (C) 2008 Zsolt Foldvari -# Copyright (C) 2013 Doug Blank -# Copyright (C) 2017 Nick Hall +# Copyright (C) 2008 Zsolt Foldvari +# Copyright (C) 2013 Doug Blank +# Copyright (C) 2017,2024 Nick Hall # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -70,6 +70,16 @@ def __init__(self, name=None, value=None, ranges=None): # Current use of StyledTextTag is such that a shallow copy suffices. self.ranges = ranges + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to convert the elements of `ranges` into tuples. + """ + attr_dict["ranges"] = [tuple(item) for item in attr_dict["ranges"]] + super().set_object_state(attr_dict) + def serialize(self): """Convert the object to a serialized tuple of data. diff --git a/gramps/gen/lib/tag.py b/gramps/gen/lib/tag.py index f6fe87d1e31..e47cd6ff71f 100644 --- a/gramps/gen/lib/tag.py +++ b/gramps/gen/lib/tag.py @@ -1,8 +1,8 @@ # # Gramps - a GTK+/GNOME based genealogy program # -# Copyright (C) 2010,2017 Nick Hall -# Copyright (C) 2013 Doug Blank +# Copyright (C) 2010,2017,2024 Nick Hall +# Copyright (C) 2013 Doug Blank # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -108,6 +108,32 @@ def unserialize(self, data): ) = data return self + def get_object_state(self): + """ + Get the current object state as a dictionary. + + We override this method to handle the `name`, `color` and `priority` + properties. + """ + attr_dict = super().get_object_state() + attr_dict["name"] = self.__name + attr_dict["color"] = self.__color + attr_dict["priority"] = self.__priority + return attr_dict + + def set_object_state(self, attr_dict): + """ + Set the current object state using information provided in the given + dictionary. + + We override this method to handle the `name`, `color` and `priority` + properties. + """ + self.__name = attr_dict.pop("name") + self.__color = attr_dict.pop("color") + self.__priority = attr_dict.pop("priority") + super().set_object_state(attr_dict) + @classmethod def get_schema(cls): """ diff --git a/gramps/gen/lib/test/serialize_test.py b/gramps/gen/lib/test/serialize_test.py index 203fa2ba6f1..bbc44e8324f 100644 --- a/gramps/gen/lib/test/serialize_test.py +++ b/gramps/gen/lib/test/serialize_test.py @@ -50,11 +50,6 @@ def test_from_json(self): obj = from_json(data) self.assertEqual(self.object.serialize(), obj.serialize()) - def test_from_empty_json(self): - data = '{"_class": "%s"}' % self.cls.__name__ - obj = from_json(data) - self.assertEqual(self.object.serialize(), obj.serialize()) - class PersonCheck(unittest.TestCase, BaseCheck): def setUp(self):