__init__.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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(1919, 12, 23)
  25. >>> alice.death_date = datetime.date(2019, 11, 1)
  26. >>> alice
  27. Person(alice, Alice Test, *1919-12-23, †2019-11-01)
  28. >>> str(alice)
  29. 'Alice Test (*1919-12-23, †2019-11-01)'
  30. >>> bob = Person('bob')
  31. >>> bob.name = 'Bob Test'
  32. >>> alice.father = bob
  33. """
  34. def __init__(self, identifier: str):
  35. self.__identifier: str = identifier
  36. self.name: typing.Optional[str] = None
  37. self.birth_date: typing.Optional[datetime.date] = None
  38. self.death_date: typing.Optional[datetime.date] = None
  39. self.mother: typing.Optional["Person"] = None
  40. self.father: typing.Optional["Person"] = None
  41. @property
  42. def identifier(self) -> str:
  43. return self.__identifier
  44. def __hash__(self) -> int:
  45. return hash(self.__identifier)
  46. def __repr__(self) -> str:
  47. """
  48. >>> p = Person("max-mustermann")
  49. >>> repr(p)
  50. 'Person(max-mustermann)'
  51. >>> p.name = "Hr. Mustermann"
  52. >>> repr(p)
  53. 'Person(max-mustermann, Hr. Mustermann)'
  54. >>> p.name = "Max Mustermann"
  55. >>> repr(p)
  56. 'Person(max-mustermann, Max Mustermann)'
  57. >>> p.birth_date = datetime.date(1876, 2, 1)
  58. >>> p.death_date = datetime.date(1976, 2, 1)
  59. >>> repr(p)
  60. 'Person(max-mustermann, Max Mustermann, *1876-02-01, †1976-02-01)'
  61. """
  62. return "{}({})".format(
  63. type(self).__name__,
  64. ", ".join(
  65. filter(
  66. None,
  67. (
  68. self.identifier,
  69. self.name,
  70. "*" + self.birth_date.isoformat()
  71. if self.birth_date is not None
  72. else None,
  73. "†" + self.death_date.isoformat()
  74. if self.death_date is not None
  75. else None,
  76. ),
  77. )
  78. ),
  79. )
  80. def __str__(self) -> str:
  81. """
  82. >>> p = Person("max-mustermann")
  83. >>> p.name = "Max Mustermann"
  84. >>> str(p)
  85. 'Max Mustermann'
  86. >>> p.birth_date = datetime.date(1876, 2, 1)
  87. >>> str(p)
  88. 'Max Mustermann (*1876-02-01)'
  89. >>> p.death_date = datetime.date(1976, 1, 2)
  90. >>> str(p)
  91. 'Max Mustermann (*1876-02-01, †1976-01-02)'
  92. """
  93. attrs = []
  94. if self.birth_date is not None:
  95. attrs.append("*{}".format(self.birth_date.isoformat()))
  96. if self.death_date is not None:
  97. attrs.append("†{}".format(self.death_date.isoformat()))
  98. return (self.name or "unnamed") + (
  99. " ({})".format(", ".join(attrs)) if attrs else ""
  100. )
  101. def __eq__(self, other: "Person") -> bool:
  102. """
  103. >>> maxl = Person("max")
  104. >>> maxl.name = "Max Mustermann"
  105. >>> maxl == Person("max")
  106. True
  107. >>> erika = Person("erika")
  108. >>> erika.name = "Max Mustermann"
  109. >>> maxl == erika
  110. False
  111. """
  112. return self.identifier == other.identifier
  113. def merge(self, person: "Person") -> None:
  114. """
  115. >>> p1 = Person("max")
  116. >>> p1.name = "Max Mustermann"
  117. >>> str(p1)
  118. 'Max Mustermann'
  119. >>> p2 = Person("max2")
  120. >>> p2.birth_date = datetime.date(1876, 2, 1)
  121. >>> p2.death_date = datetime.date(1976, 2, 1)
  122. >>> p2.mother = Person("mother")
  123. >>> p2.father = Person("father")
  124. >>> str(p2)
  125. 'unnamed (*1876-02-01, †1976-02-01)'
  126. add attributes of p2 to p1:
  127. >>> p1.merge(p2)
  128. >>> str(p1)
  129. 'Max Mustermann (*1876-02-01, †1976-02-01)'
  130. >>> p1.mother, p1.father
  131. (Person(mother), Person(father))
  132. p2 is unchanged:
  133. >>> str(p2)
  134. 'unnamed (*1876-02-01, †1976-02-01)'
  135. """
  136. for attr in ["name", "birth_date", "death_date", "mother", "father"]:
  137. if getattr(person, attr) is not None:
  138. setattr(self, attr, getattr(person, attr))
  139. @property
  140. def parents(self) -> typing.Tuple["Person"]:
  141. """
  142. >>> p = Person("max")
  143. >>> p.parents
  144. ()
  145. >>> p.mother = Person("mum")
  146. >>> p.parents
  147. (Person(mum),)
  148. >>> p.father = Person("dad")
  149. >>> p.parents
  150. (Person(mum), Person(dad))
  151. >>> p.mother = None
  152. >>> p.parents
  153. (Person(dad),)
  154. """
  155. return tuple(filter(None, [self.mother, self.father]))
  156. class PersonCollection:
  157. """
  158. >>> bob = Person('bob')
  159. >>> bob.name = 'Bob Test'
  160. >>> alice = Person('alice')
  161. >>> alice.name = 'Alice Test'
  162. >>> alice.father = bob
  163. >>> collection = PersonCollection()
  164. >>> collection.add_person(alice)
  165. Person(alice, Alice Test)
  166. >>> for person in collection:
  167. ... print(person.name)
  168. Bob Test
  169. Alice Test
  170. """
  171. def __init__(self):
  172. self._persons = {}
  173. self.__children = collections.defaultdict(set)
  174. self.__it = None
  175. def __getitem__(self, identifier: str) -> Person:
  176. """
  177. >>> c = PersonCollection()
  178. >>> c.add_person(Person("alice"))
  179. Person(alice)
  180. >>> c["alice"]
  181. Person(alice)
  182. """
  183. return self._persons[identifier]
  184. def add_person(self, person: Person) -> Person:
  185. """
  186. >>> c = PersonCollection()
  187. >>> c.add_person(Person("alice"))
  188. Person(alice)
  189. >>> c.add_person(Person("bob"))
  190. Person(bob)
  191. >>> c["bob"]
  192. Person(bob)
  193. >>> bob = Person("bob")
  194. >>> bob.birth_date = datetime.date(2010, 2, 3)
  195. >>> bob.mother = Person("bob-mum")
  196. >>> bob.father = Person("bob-dad")
  197. >>> c.add_person(bob)
  198. Person(bob, *2010-02-03)
  199. >>> list(c)
  200. [Person(alice), Person(bob, *2010-02-03), Person(bob-mum), Person(bob-dad)]
  201. """
  202. if person.mother is not None:
  203. person.mother = self.add_person(person.mother)
  204. self.__children[person.mother.identifier].add(person)
  205. if person.father is not None:
  206. person.father = self.add_person(person.father)
  207. self.__children[person.father.identifier].add(person)
  208. if person.identifier not in self._persons:
  209. self._persons[person.identifier] = person
  210. return person
  211. existing_person = self._persons[person.identifier]
  212. existing_person.merge(person)
  213. return existing_person
  214. def get_children(self, parent: typing.Union[Person, str]) -> typing.Set[Person]:
  215. """
  216. >>> c = PersonCollection()
  217. >>> alice = Person('alice')
  218. >>> alice.father = Person('bob')
  219. >>> c.add_person(alice)
  220. Person(alice)
  221. >>> c.get_children(alice)
  222. set()
  223. >>> c.get_children('bob')
  224. {Person(alice)}
  225. >>> carol = Person('carol')
  226. >>> carol.father = c['bob']
  227. >>> c.add_person(carol)
  228. Person(carol)
  229. >>> sorted(c.get_children('bob'), key=lambda p: p.identifier)
  230. [Person(alice), Person(carol)]
  231. does not support change / removal of parents:
  232. >>> carol.father = Person('other-bob')
  233. >>> c.add_person(carol)
  234. Person(carol)
  235. >>> c.get_children('other-bob')
  236. {Person(carol)}
  237. >>> sorted(c.get_children('bob'), key=lambda p: p.identifier)
  238. [Person(alice), Person(carol)]
  239. """
  240. if isinstance(parent, str):
  241. return self.__children[parent]
  242. return self.get_children(parent.identifier)
  243. def get_coparents(self, parent: typing.Union[Person, str]) -> typing.Set[Person]:
  244. """
  245. >>> c = PersonCollection()
  246. >>> alice = Person('alice')
  247. >>> alice.father = Person('bob')
  248. >>> c.add_person(alice)
  249. Person(alice)
  250. >>> c.get_coparents('bob')
  251. set()
  252. >>> alice.mother = Person('carol')
  253. >>> c.add_person(alice)
  254. Person(alice)
  255. >>> c.get_coparents('bob')
  256. {Person(carol)}
  257. >>> c.get_coparents(c['carol'])
  258. {Person(bob)}
  259. """
  260. if isinstance(parent, Person):
  261. return self.get_coparents(parent.identifier)
  262. coparents = set()
  263. for child in self.get_children(parent):
  264. for coparent in child.parents:
  265. if coparent.identifier != parent:
  266. coparents.add(coparent)
  267. return coparents
  268. def __iter__(self) -> "PersonCollection":
  269. """
  270. >>> c = PersonCollection()
  271. >>> for identifier in ['alice', 'bob', 'charlie']:
  272. ... c.add_person(Person(identifier))
  273. Person(alice)
  274. Person(bob)
  275. Person(charlie)
  276. >>> list(c)
  277. [Person(alice), Person(bob), Person(charlie)]
  278. """
  279. self.__it = iter(self._persons.values())
  280. return self
  281. def __next__(self) -> Person:
  282. return next(self.__it)