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 ..plug import BasePluginManager
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
_ = 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):
"""
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)
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)