Move struct-based API to gen.lib.struct

This commit is contained in:
Doug Blank 2015-12-26 21:00:37 -05:00
parent 664359e681
commit 84fc198a6d
2 changed files with 348 additions and 423 deletions

348
gramps/gen/lib/struct.py Normal file
View File

@ -0,0 +1,348 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2015 Gramps Development Team
#
# 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
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
from gramps.gen.lib.handle import HandleClass
class Struct(object):
"""
Class for getting and setting parts of a struct by dotted path.
>>> s = Struct({"gramps_id": "I0001", ...}, database)
>>> s.primary_name.surname_list[0].surname
Jones
>>> s.primary_name.surname_list[0].surname = "Smith"
>>> s.primary_name.surname_list[0]surname
Smith
"""
def __init__(self, struct, db=None):
self.struct = struct
self.db = db
if self.db:
self.transaction = db.get_transaction_class()
else:
self.transaction = None
@classmethod
def wrap(cls, instance, db=None):
return Struct(instance.to_struct(), db)
def __setitem__(self, item, value):
self.struct[item] = value
def __eq__(self, other):
if isinstance(other, Struct):
return self.struct == other.struct
elif isinstance(self.struct, list):
## FIXME: self.struct can be a dict, list, etc
for item in self.struct:
if item == other:
return True
return False
else:
return self.struct == other
def __lt__(self, other):
if isinstance(other, Struct):
return self.struct < other.struct
else:
return self.struct < other
def __gt__(self, other):
if isinstance(other, Struct):
return self.struct > other.struct
else:
return self.struct > other
def __le__(self, other):
if isinstance(other, Struct):
return self.struct <= other.struct
else:
return self.struct <= other
def __ge__(self, other):
if isinstance(other, Struct):
return self.struct >= other.struct
else:
return self.struct >= other
def __ne__(self, other):
if isinstance(other, Struct):
return self.struct != other.struct
else:
return self.struct != other
def __len__(self):
return len(self.struct)
def __contains__(self, item):
return item in self.struct
def __call__(self, *args, **kwargs):
"""
You can use this to select and filter a list of structs.
args are dotted strings of what componets of the structs to
select, and kwargs is the selection criteria, double-under
scores represent dots.
If no args are given, all are provided.
"""
selected = self.struct # better be dicts
# First, find elements of the list that match any given
# selection criteria:
selected = self.struct # assume dicts
# assume True
to_delete = []
for key in kwargs: # value="Social Security Number"
parts = self.getitem_from_path(key.split("__")) # returns all
# This will return a list; we keep the ones that match
for p in range(len(parts)):
# if it matches, keep it:
if parts[p] != kwargs[key]:
to_delete.append(p)
# delete from highest to lowest, to use pop:
for p in reversed(to_delete):
selected.pop(p)
# now select which parts to show:
if args: # just some of the parts, ["type.string", ...]
results = []
for select in selected: # dict in dicts
parts = []
for item in args: # ["type.string"]
items = item.split(".") # "type.string"
values = Struct(select, self.db).getitem_from_path(items)
if values:
parts.append((item, values))
results.append(parts) # return [["type.string", "Social Security Number"], ...]
else: # return all
results = selected
# return them
return results
def select(self, thing1, thing2):
if thing2 == "*":
return thing1
elif thing2 in thing1:
return thing2
elif thing1 == thing2:
return thing1
else:
return None
def __getattr__(self, attr):
"""
Called when getattr fails. Lookup attr in struct; returns Struct
if more struct.
>>> Struct({}, db).primary_name
returns: Struct([], db) or value
struct can be list/tuple, dict with _class, or value (including dict).
self.setitem_from_path(path, v) should be used to set value of
item.
"""
if isinstance(self.struct, dict) and "_class" in self.struct.keys():
# this is representing an object
if attr in self.struct.keys():
return self.handle_join(self.struct[attr])
else:
raise AttributeError("attempt to access a property of an object: '%s', '%s'" % (self.struct, attr))
elif isinstance(self.struct, HandleClass):
struct = self.handle_join(self.struct)
return getattr(struct, attr)
elif isinstance(self.struct, (list, tuple)):
# get first item in list that matches:
sublist = [getattr(Struct(item, self.db), attr) for item in self.struct]
return Struct(sublist, self.db)
else:
# better be a property of the list/tuple/dict/value:
return getattr(self.struct, attr)
def __getitem__(self, item):
"""
Called when getitem fails. Lookup item in struct; returns Struct
if more struct.
>>> Struct({}, db)[12]
returns: Struct([], db) or value
struct can be list/tuple, dict with _class, or value (including dict).
"""
if isinstance(item, str) and isinstance(self.struct, (list, tuple)):
fields = [field.strip() for field in item.split(",")]
results = []
for item in self.struct:
sublist = [getattr(Struct(item, self.db), field) for field in fields]
if any(sublist):
results.append(tuple(sublist))
return results if results else None
else:
return self.handle_join(self.struct[item])
def getitem_from_path(self, items):
"""
path is a list
"""
current = self
for item in items:
current = getattr(current, item)
return current
def get_object_from_handle(self, handle):
return self.db.get_from_name_and_handle(handle.classname, str(handle))
def handle_join(self, item):
"""
If the item is a handle, look up reference object.
"""
if isinstance(item, HandleClass) and self.db:
obj = self.get_object_from_handle(item)
if obj:
return Struct(obj.to_struct(), self.db)
else:
raise AttributeError("missing object: %s" % item)
elif isinstance(item, (list, tuple)):
return Struct(item, self.db)
elif isinstance(item, dict) and "_class" in item.keys():
return Struct(item, self.db)
else:
return item
def setitem(self, path, value, trans=None):
"""
Given a path to a struct part, set the last part to value.
>>> Struct(struct).setitem("primary_name.surname_list.0.surname", "Smith")
"""
return self.setitem_from_path(parse(path), value, trans)
def primary_object_q(self, _class):
return _class in ["Person", "Family", "Event", "Source", "Citation",
"Tag", "Repository", "Note", "Media"]
def setitem_from_path(self, path, value, trans=None):
"""
Given a path to a struct part, set the last part to value.
>>> Struct(struct).setitem_from_path(["primary_name", "surname_list", "[0]", "surname"], "Smith", transaction)
"""
path, item = path[:-1], path[-1]
if item.startswith("["):
item = item[1:-1]
struct = self.struct
primary_obj = struct
for p in range(len(path)):
part = path[p]
if part.startswith("["): # getitem
struct = struct[eval(part[1:-1])] # for int or string use
else: # getattr
struct = struct[part]
if struct is None: # invalid part to set, skip
return
if isinstance(struct, HandleClass):
struct = self.get_object_from_handle(struct).to_struct()
# keep track of primary object for update, below
if isinstance(struct, dict) and "_class" in struct and self.primary_object_q(struct["_class"]):
primary_obj = struct
# struct is now set
if item in struct and isinstance(struct[item], list): # assigning to a list
if value is not None:
struct[item].append(value) # we append the value
else:
struct[item] = []
elif isinstance(struct, (list, tuple)):
pos = int(item)
if pos < len(struct):
if value is not None:
struct[int(item)] = value
else:
struct.pop(int(item))
elif isinstance(struct, dict):
if item in struct.keys():
struct[item] = value
elif hasattr(struct, item):
setattr(struct, item, value)
else:
return
self.update_db(primary_obj, trans)
def update_db(self, struct, trans=None):
if self.db:
if trans is None:
with self.transaction("Struct Update", self.db, batch=True) as trans:
new_obj = self.from_struct(struct)
name, handle = struct["_class"], struct["handle"]
old_obj = self.db.get_from_name_and_handle(name, handle)
if old_obj:
commit_func = self.db._tables[name]["commit_func"]
commit_func(new_obj, trans)
else:
add_func = self.db._tables[name]["add_func"]
add_func(new_obj, trans)
else:
new_obj = self.from_struct(struct)
name, handle = struct["_class"], struct["handle"]
old_obj = self.db.get_from_name_and_handle(name, handle)
if old_obj:
commit_func = self.db._tables[name]["commit_func"]
commit_func(new_obj, trans)
else:
add_func = self.db._tables[name]["add_func"]
add_func(new_obj, trans)
def from_struct(struct):
"""
Given a struct with metadata, create a Gramps object.
"""
from gramps.gen.lib import (Person, Family, Event, Source, Place, Citation,
Repository, MediaObject, Note, Tag)
if isinstance(struct, dict):
if "_class" in struct.keys():
if struct["_class"] == "Person":
return Person.create(Person.from_struct(struct))
elif struct["_class"] == "Family":
return Family.create(Family.from_struct(struct))
elif struct["_class"] == "Event":
return Event.create(Event.from_struct(struct))
elif struct["_class"] == "Source":
return Source.create(Source.from_struct(struct))
elif struct["_class"] == "Place":
return Place.create(Place.from_struct(struct))
elif struct["_class"] == "Citation":
return Citation.create(Citation.from_struct(struct))
elif struct["_class"] == "Repository":
return Repository.create(Repository.from_struct(struct))
elif struct["_class"] == "MediaObject":
return MediaObject.create(MediaObject.from_struct(struct))
elif struct["_class"] == "Note":
return Note.create(Note.from_struct(struct))
elif struct["_class"] == "Tag":
return Tag.create(Tag.from_struct(struct))
raise AttributeError("invalid struct: %s" % struct)
def __str__(self):
return str(self.struct)
def __repr__(self):
if "_class" in self.struct:
return "<%s struct instance>" % self._class
else:
return repr(self.struct)

View File

@ -29,95 +29,9 @@ from ..dbstate import DbState
from gramps.cli.grampscli import CLIManager from gramps.cli.grampscli import CLIManager
from ..plug import BasePluginManager from ..plug import BasePluginManager
from gramps.plugins.database.dictionarydb import DictionaryDb from gramps.plugins.database.dictionarydb import DictionaryDb
from gramps.gen.lib.handle import HandleClass, Handle
from gramps.gen.lib import *
from gramps.gen.lib.personref import PersonRef
from gramps.gen.lib.eventref import EventRef
from ..const import GRAMPS_LOCALE as glocale from ..const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext _ = glocale.translation.gettext
def get_schema(cls):
"""
Given a gramps.gen.lib class, or class name, return a dictionary
containing the schema. The schema is a dictionary of fieldname
keys with type as value.
Also, the schema includes the same metadata fields as does struct,
including "_class", returned as real class of cls.
"""
orig_cls = cls
# Get type as a gramps.gen.lib string name
if isinstance(cls, type):
cls = cls().__class__.__name__
elif isinstance(cls, object) and not isinstance(cls, str):
cls = cls.__class__.__name__
### Is there a Path?
if "." in cls:
items = parse(cls) # "Person.alternate_names"
cls, path = items[0], items[1:]
else:
path = []
# Now get the schema
if cls in ("str", "int", "bool", "float", "long", "list"):
schema = orig_cls
elif cls == "Person":
schema = {
"_class": Person,
"handle": Handle("Person", "PERSON-HANDLE"),
"gramps_id": str,
"gender": int,
"primary_name": Name,
"alternate_names": [Name],
"death_ref_index": int,
"birth_ref_index": int,
"event_ref_list": [EventRef],
"family_list": [Handle("Family", "FAMILY-HANDLE")],
"parent_family_list": [Handle("Family", "FAMILY-HANDLE")],
"media_list": [MediaRef],
"address_list": [Address],
"attribute_list": [Attribute],
"urls": [Url],
"lds_ord_list": [LdsOrd],
"citation_list": [Handle("Citation", "CITATION-HANDLE")],
"note_list": [Handle("Note", "NOTE-HANDLE")],
"change": int,
"tag_list": [Handle("Tag", "TAG-HANDLE")],
"private": bool,
"person_ref_list": [PersonRef]
}
elif cls == "Name":
schema = {
"_class": Name,
"private": bool,
"citation_list": [Handle("Citation", "CITATION-HANDLE")],
"note_list": [Handle("Note", "NOTE-HANDLE")],
"date": Date,
"first_name": str,
"surname_list": [Surname],
"suffix": str,
"title": str,
"type": NameType,
"group_as": str,
"sort_as": int,
"display_as": int,
"call": str,
"nick": str,
"famnick": str,
}
else:
raise AttributeError("unknown class '%s'" % cls)
# walk down path, if given:
for p in range(len(path)):
# could be a list
item = path[p]
if isinstance(schema, (list, tuple)):
schema = schema[int(item)]
else:
schema = schema[item]
if isinstance(schema, type):
schema = get_schema(schema)
return schema
def parse(string): def parse(string):
""" """
Break a string up into a struct-path. Used by get_schema() and setitem(). Break a string up into a struct-path. Used by get_schema() and setitem().
@ -318,340 +232,3 @@ def diff_db_to_file(old_db, filename, user=None):
diffs, m_old, m_new = diff_dbs(old_db, new_db, user) diffs, m_old, m_new = diff_dbs(old_db, new_db, user)
return diffs, m_old, m_new return diffs, m_old, m_new
def from_struct(struct):
"""
Given a struct with metadata, create a Gramps object.
"""
from gramps.gen.lib import (Person, Family, Event, Source, Place, Citation,
Repository, MediaObject, Note, Tag)
if isinstance(struct, dict):
if "_class" in struct.keys():
if struct["_class"] == "Person":
return Person.create(Person.from_struct(struct))
elif struct["_class"] == "Family":
return Family.create(Family.from_struct(struct))
elif struct["_class"] == "Event":
return Event.create(Event.from_struct(struct))
elif struct["_class"] == "Source":
return Source.create(Source.from_struct(struct))
elif struct["_class"] == "Place":
return Place.create(Place.from_struct(struct))
elif struct["_class"] == "Citation":
return Citation.create(Citation.from_struct(struct))
elif struct["_class"] == "Repository":
return Repository.create(Repository.from_struct(struct))
elif struct["_class"] == "MediaObject":
return MediaObject.create(MediaObject.from_struct(struct))
elif struct["_class"] == "Note":
return Note.create(Note.from_struct(struct))
elif struct["_class"] == "Tag":
return Tag.create(Tag.from_struct(struct))
raise AttributeError("invalid struct: %s" % struct)
def get_dependencies(struct):
"""
Walk the struct recursively, getting all dependencies on other
objects.
"""
if isinstance(struct, HandleClass):
return set([(struct.classname, str(struct))])
elif isinstance(struct, (tuple, list)):
retval = set([])
for item in struct:
retval.update(get_dependencies(item))
return retval
elif isinstance(struct, dict):
retval = set([])
for key in struct.keys():
retval.update(get_dependencies(struct[key]))
return retval
else:
return set([])
class Struct(object):
"""
Class for getting and setting parts of a struct by dotted path.
>>> s = Struct({"gramps_id": "I0001", ...}, database)
>>> s.primary_name.surname_list[0].surname
Jones
>>> s.primary_name.surname_list[0].surname = "Smith"
>>> s.primary_name.surname_list[0]surname
Smith
"""
def __init__(self, struct, db=None):
self.struct = struct
self.db = db
if self.db:
self.transaction = db.get_transaction_class()
else:
self.transaction = None
def __setitem__(self, item, value):
self.struct[item] = value
def __eq__(self, other):
if isinstance(other, Struct):
return self.struct == other.struct
elif isinstance(self.struct, list):
## FIXME: self.struct can be a dict, list, etc
for item in self.struct:
if item == other:
return True
return False
else:
return self.struct == other
def __lt__(self, other):
if isinstance(other, Struct):
return self.struct < other.struct
else:
return self.struct < other
def __gt__(self, other):
if isinstance(other, Struct):
return self.struct > other.struct
else:
return self.struct > other
def __le__(self, other):
if isinstance(other, Struct):
return self.struct <= other.struct
else:
return self.struct <= other
def __ge__(self, other):
if isinstance(other, Struct):
return self.struct >= other.struct
else:
return self.struct >= other
def __ne__(self, other):
if isinstance(other, Struct):
return self.struct != other.struct
else:
return self.struct != other
def __len__(self):
return len(self.struct)
def __contains__(self, item):
return item in self.struct
def __call__(self, *args, **kwargs):
"""
You can use this to select and filter a list of structs.
args are dotted strings of what componets of the structs to
select, and kwargs is the selection criteria, double-under
scores represent dots.
If no args are given, all are provided.
"""
selected = self.struct # better be dicts
# First, find elements of the list that match any given
# selection criteria:
selected = self.struct # assume dicts
# assume True
to_delete = []
for key in kwargs: # value="Social Security Number"
parts = self.getitem_from_path(key.split("__")) # returns all
# This will return a list; we keep the ones that match
for p in range(len(parts)):
# if it matches, keep it:
if parts[p] != kwargs[key]:
to_delete.append(p)
# delete from highest to lowest, to use pop:
for p in reversed(to_delete):
selected.pop(p)
# now select which parts to show:
if args: # just some of the parts, ["type.string", ...]
results = []
for select in selected: # dict in dicts
parts = []
for item in args: # ["type.string"]
items = item.split(".") # "type.string"
values = Struct(select, self.db).getitem_from_path(items)
if values:
parts.append((item, values))
results.append(parts) # return [["type.string", "Social Security Number"], ...]
else: # return all
results = selected
# return them
return results
def select(self, thing1, thing2):
if thing2 == "*":
return thing1
elif thing2 in thing1:
return thing2
elif thing1 == thing2:
return thing1
else:
return None
def __getattr__(self, attr):
"""
Called when getattr fails. Lookup attr in struct; returns Struct
if more struct.
>>> Struct({}, db).primary_name
returns: Struct([], db) or value
struct can be list/tuple, dict with _class, or value (including dict).
self.setitem_from_path(path, v) should be used to set value of
item.
"""
if isinstance(self.struct, dict) and "_class" in self.struct.keys():
# this is representing an object
if attr in self.struct.keys():
return self.handle_join(self.struct[attr])
else:
raise AttributeError("attempt to access a property of an object: '%s', '%s'" % (self.struct, attr))
elif isinstance(self.struct, HandleClass):
struct = self.handle_join(self.struct)
return getattr(struct, attr)
elif isinstance(self.struct, (list, tuple)):
# get first item in list that matches:
sublist = [getattr(Struct(item, self.db), attr) for item in self.struct]
return Struct(sublist, self.db)
else:
# better be a property of the list/tuple/dict/value:
return getattr(self.struct, attr)
def __getitem__(self, item):
"""
Called when getitem fails. Lookup item in struct; returns Struct
if more struct.
>>> Struct({}, db)[12]
returns: Struct([], db) or value
struct can be list/tuple, dict with _class, or value (including dict).
"""
if isinstance(item, str) and isinstance(self.struct, (list, tuple)):
fields = [field.strip() for field in item.split(",")]
results = []
for item in self.struct:
sublist = [getattr(Struct(item, self.db), field) for field in fields]
if any(sublist):
results.append(tuple(sublist))
return results if results else None
else:
return self.handle_join(self.struct[item])
def getitem_from_path(self, items):
"""
path is a list
"""
current = self
for item in items:
current = getattr(current, item)
return current
def get_object_from_handle(self, handle):
return self.db.get_from_name_and_handle(handle.classname, str(handle))
def handle_join(self, item):
"""
If the item is a handle, look up reference object.
"""
if isinstance(item, HandleClass) and self.db:
obj = self.get_object_from_handle(item)
if obj:
return Struct(obj.to_struct(), self.db)
else:
raise AttributeError("missing object: %s" % item)
elif isinstance(item, (list, tuple)):
return Struct(item, self.db)
elif isinstance(item, dict) and "_class" in item.keys():
return Struct(item, self.db)
else:
return item
def setitem(self, path, value, trans=None):
"""
Given a path to a struct part, set the last part to value.
>>> Struct(struct).setitem("primary_name.surname_list.0.surname", "Smith")
"""
return self.setitem_from_path(parse(path), value, trans)
def primary_object_q(self, _class):
return _class in ["Person", "Family", "Event", "Source", "Citation",
"Tag", "Repository", "Note", "Media"]
def setitem_from_path(self, path, value, trans=None):
"""
Given a path to a struct part, set the last part to value.
>>> Struct(struct).setitem_from_path(["primary_name", "surname_list", "[0]", "surname"], "Smith", transaction)
"""
path, item = path[:-1], path[-1]
if item.startswith("["):
item = item[1:-1]
struct = self.struct
primary_obj = struct
for p in range(len(path)):
part = path[p]
if part.startswith("["): # getitem
struct = struct[eval(part[1:-1])] # for int or string use
else: # getattr
struct = struct[part]
if struct is None: # invalid part to set, skip
return
if isinstance(struct, HandleClass):
struct = self.get_object_from_handle(struct).to_struct()
# keep track of primary object for update, below
if isinstance(struct, dict) and "_class" in struct and self.primary_object_q(struct["_class"]):
primary_obj = struct
# struct is now set
if item in struct and isinstance(struct[item], list): # assigning to a list
if value is not None:
struct[item].append(value) # we append the value
else:
struct[item] = []
elif isinstance(struct, (list, tuple)):
pos = int(item)
if pos < len(struct):
if value is not None:
struct[int(item)] = value
else:
struct.pop(int(item))
elif isinstance(struct, dict):
if item in struct.keys():
struct[item] = value
elif hasattr(struct, item):
setattr(struct, item, value)
else:
return
self.update_db(primary_obj, trans)
def update_db(self, struct, trans=None):
if self.db:
if trans is None:
with self.transaction("Struct Update", self.db, batch=True) as trans:
new_obj = from_struct(struct)
name, handle = struct["_class"], struct["handle"]
old_obj = self.db.get_from_name_and_handle(name, handle)
if old_obj:
commit_func = self.db._tables[name]["commit_func"]
commit_func(new_obj, trans)
else:
add_func = self.db._tables[name]["add_func"]
add_func(new_obj, trans)
else:
new_obj = from_struct(struct)
name, handle = struct["_class"], struct["handle"]
old_obj = self.db.get_from_name_and_handle(name, handle)
if old_obj:
commit_func = self.db._tables[name]["commit_func"]
commit_func(new_obj, trans)
else:
add_func = self.db._tables[name]["add_func"]
add_func(new_obj, trans)
def __str__(self):
return str(self.struct)