__init__.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. # yamify - define family trees in YAML
  2. #
  3. # Copyright (C) 2020 Fabian Peter Hammerle <fabian@hammerle.me>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. import collections
  18. import datetime
  19. import typing
  20. class Person:
  21. """
  22. >>> alice = Person('alice')
  23. >>> alice.name = 'Alice Test'
  24. >>> alice.birth_date = datetime.date(2019, 12, 23)
  25. >>> alice
  26. Person(alice, Alice Test, *2019-12-23)
  27. >>> str(alice)
  28. 'Alice Test (*2019-12-23)'
  29. >>> bob = Person('bob')
  30. >>> bob.name = 'Bob Test'
  31. >>> alice.father = bob
  32. """
  33. def __init__(self, identifier: str):
  34. self.__identifier: str = identifier
  35. self.name: typing.Optional[str] = None
  36. self.birth_date: typing.Optional[datetime.date] = None
  37. self.mother: typing.Optional["Person"] = None
  38. self.father: typing.Optional["Person"] = None
  39. @property
  40. def identifier(self) -> str:
  41. return self.__identifier
  42. def __hash__(self) -> int:
  43. return hash(self.__identifier)
  44. def __repr__(self) -> str:
  45. """
  46. >>> p = Person("max-mustermann")
  47. >>> repr(p)
  48. 'Person(max-mustermann)'
  49. >>> p.name = "Hr. Mustermann"
  50. >>> repr(p)
  51. 'Person(max-mustermann, Hr. Mustermann)'
  52. >>> p.name = "Max Mustermann"
  53. >>> repr(p)
  54. 'Person(max-mustermann, Max Mustermann)'
  55. >>> p.birth_date = datetime.date(1976, 2, 1)
  56. >>> repr(p)
  57. 'Person(max-mustermann, Max Mustermann, *1976-02-01)'
  58. """
  59. return "{}({})".format(
  60. type(self).__name__,
  61. ", ".join(
  62. filter(
  63. None,
  64. (
  65. self.identifier,
  66. self.name,
  67. "*" + self.birth_date.isoformat()
  68. if self.birth_date is not None
  69. else None,
  70. ),
  71. )
  72. ),
  73. )
  74. def __str__(self) -> str:
  75. """
  76. >>> p = Person("max-mustermann")
  77. >>> p.name = "Max Mustermann"
  78. >>> str(p)
  79. 'Max Mustermann'
  80. >>> p.birth_date = datetime.date(1976, 2, 1)
  81. >>> str(p)
  82. 'Max Mustermann (*1976-02-01)'
  83. """
  84. return (self.name or "unnamed") + (
  85. " (*{})".format(self.birth_date.isoformat()) if self.birth_date else ""
  86. )
  87. def __eq__(self, other: "Person") -> bool:
  88. """
  89. >>> maxl = Person("max")
  90. >>> maxl.name = "Max Mustermann"
  91. >>> maxl == Person("max")
  92. True
  93. >>> erika = Person("erika")
  94. >>> erika.name = "Max Mustermann"
  95. >>> maxl == erika
  96. False
  97. """
  98. return self.identifier == other.identifier
  99. def merge(self, person: "Person") -> None:
  100. """
  101. >>> p1 = Person("max")
  102. >>> p1.name = "Max Mustermann"
  103. >>> str(p1)
  104. 'Max Mustermann'
  105. >>> p2 = Person("max2")
  106. >>> p2.birth_date = datetime.date(1976, 2, 1)
  107. >>> p2.mother = Person("mother")
  108. >>> p2.father = Person("father")
  109. >>> str(p2)
  110. 'unnamed (*1976-02-01)'
  111. add attributes of p2 to p1:
  112. >>> p1.merge(p2)
  113. >>> str(p1)
  114. 'Max Mustermann (*1976-02-01)'
  115. >>> p1.mother, p1.father
  116. (Person(mother), Person(father))
  117. p2 is unchanged:
  118. >>> str(p2)
  119. 'unnamed (*1976-02-01)'
  120. """
  121. for attr in ["name", "birth_date", "mother", "father"]:
  122. if getattr(person, attr) is not None:
  123. setattr(self, attr, getattr(person, attr))
  124. @property
  125. def parents(self) -> typing.Tuple["Person"]:
  126. """
  127. >>> p = Person("max")
  128. >>> p.parents
  129. ()
  130. >>> p.mother = Person("mum")
  131. >>> p.parents
  132. (Person(mum),)
  133. >>> p.father = Person("dad")
  134. >>> p.parents
  135. (Person(mum), Person(dad))
  136. >>> p.mother = None
  137. >>> p.parents
  138. (Person(dad),)
  139. """
  140. return tuple(filter(None, [self.mother, self.father]))
  141. class PersonCollection:
  142. """
  143. >>> bob = Person('bob')
  144. >>> bob.name = 'Bob Test'
  145. >>> alice = Person('alice')
  146. >>> alice.name = 'Alice Test'
  147. >>> alice.father = bob
  148. >>> collection = PersonCollection()
  149. >>> collection.add_person(alice)
  150. Person(alice, Alice Test)
  151. >>> for person in collection:
  152. ... print(person.name)
  153. Bob Test
  154. Alice Test
  155. """
  156. def __init__(self):
  157. self._persons = {}
  158. self.__children = collections.defaultdict(set)
  159. self.__it = None
  160. def __getitem__(self, identifier: str) -> Person:
  161. """
  162. >>> c = PersonCollection()
  163. >>> c.add_person(Person("alice"))
  164. Person(alice)
  165. >>> c["alice"]
  166. Person(alice)
  167. """
  168. return self._persons[identifier]
  169. def add_person(self, person: Person) -> Person:
  170. """
  171. >>> c = PersonCollection()
  172. >>> c.add_person(Person("alice"))
  173. Person(alice)
  174. >>> c.add_person(Person("bob"))
  175. Person(bob)
  176. >>> c["bob"]
  177. Person(bob)
  178. >>> bob = Person("bob")
  179. >>> bob.birth_date = datetime.date(2010, 2, 3)
  180. >>> bob.mother = Person("bob-mum")
  181. >>> bob.father = Person("bob-dad")
  182. >>> c.add_person(bob)
  183. Person(bob, *2010-02-03)
  184. >>> list(c)
  185. [Person(alice), Person(bob, *2010-02-03), Person(bob-mum), Person(bob-dad)]
  186. """
  187. if person.mother is not None:
  188. person.mother = self.add_person(person.mother)
  189. self.__children[person.mother.identifier].add(person)
  190. if person.father is not None:
  191. person.father = self.add_person(person.father)
  192. self.__children[person.father.identifier].add(person)
  193. if person.identifier not in self._persons:
  194. self._persons[person.identifier] = person
  195. return person
  196. existing_person = self._persons[person.identifier]
  197. existing_person.merge(person)
  198. return existing_person
  199. def get_children(self, parent: typing.Union[Person, str]) -> typing.Set[Person]:
  200. """
  201. >>> c = PersonCollection()
  202. >>> alice = Person('alice')
  203. >>> alice.father = Person('bob')
  204. >>> c.add_person(alice)
  205. Person(alice)
  206. >>> c.get_children(alice)
  207. set()
  208. >>> c.get_children('bob')
  209. {Person(alice)}
  210. >>> carol = Person('carol')
  211. >>> carol.father = c['bob']
  212. >>> c.add_person(carol)
  213. Person(carol)
  214. >>> sorted(c.get_children('bob'), key=lambda p: p.identifier)
  215. [Person(alice), Person(carol)]
  216. does not support change / removal of parents:
  217. >>> carol.father = Person('other-bob')
  218. >>> c.add_person(carol)
  219. Person(carol)
  220. >>> c.get_children('other-bob')
  221. {Person(carol)}
  222. >>> sorted(c.get_children('bob'), key=lambda p: p.identifier)
  223. [Person(alice), Person(carol)]
  224. """
  225. if isinstance(parent, str):
  226. return self.__children[parent]
  227. return self.get_children(parent.identifier)
  228. def get_coparents(self, parent: typing.Union[Person, str]) -> typing.Set[Person]:
  229. """
  230. >>> c = PersonCollection()
  231. >>> alice = Person('alice')
  232. >>> alice.father = Person('bob')
  233. >>> c.add_person(alice)
  234. Person(alice)
  235. >>> c.get_coparents('bob')
  236. set()
  237. >>> alice.mother = Person('carol')
  238. >>> c.add_person(alice)
  239. Person(alice)
  240. >>> c.get_coparents('bob')
  241. {Person(carol)}
  242. >>> c.get_coparents(c['carol'])
  243. {Person(bob)}
  244. """
  245. if isinstance(parent, Person):
  246. return self.get_coparents(parent.identifier)
  247. coparents = set()
  248. for child in self.get_children(parent):
  249. for coparent in child.parents:
  250. if coparent.identifier != parent:
  251. coparents.add(coparent)
  252. return coparents
  253. def get_coparents_recursively(
  254. self, parent: typing.Union[Person, str]
  255. ) -> typing.Set[Person]:
  256. """
  257. >>> c = PersonCollection()
  258. >>> child_a = Person("child-a")
  259. >>> child_a.father = Person("father")
  260. >>> child_a.mother = Person("mother-a")
  261. >>> c.add_person(child_a)
  262. Person(child-a)
  263. >>> child_b = Person("child-b")
  264. >>> child_b.father = Person("father")
  265. >>> child_b.mother = Person("mother-b")
  266. >>> c.add_person(child_b)
  267. Person(child-b)
  268. >>> sorted(c.get_coparents_recursively("mother-a"), key=lambda p: p.identifier)
  269. [Person(father), Person(mother-a), Person(mother-b)]
  270. >>> c.get_coparents_recursively("child-a")
  271. set()
  272. """
  273. coparents = self.get_coparents(parent)
  274. last_len = None
  275. while len(coparents) != last_len:
  276. for coparent in set(coparents):
  277. coparents.update(self.get_coparents(coparent))
  278. last_len = len(coparents)
  279. return coparents
  280. def __iter__(self) -> "PersonCollection":
  281. """
  282. >>> c = PersonCollection()
  283. >>> for identifier in ['alice', 'bob', 'charlie']:
  284. ... c.add_person(Person(identifier))
  285. Person(alice)
  286. Person(bob)
  287. Person(charlie)
  288. >>> list(c)
  289. [Person(alice), Person(bob), Person(charlie)]
  290. """
  291. self.__it = iter(self._persons.values())
  292. return self
  293. def __next__(self) -> Person:
  294. return next(self.__it)