Browse Source

added cli entry point `yamily-list`

Fabian Peter Hammerle 4 years ago
parent
commit
f13d438e9c
8 changed files with 257 additions and 10 deletions
  1. 1 1
      Pipfile
  2. 2 1
      Pipfile.lock
  3. 1 1
      setup.py
  4. 86 0
      tests/cli/_list_test.py
  5. 76 0
      tests/person_collection.py
  6. 2 2
      yamily/__init__.py
  7. 39 0
      yamily/_cli.py
  8. 50 5
      yamily/yaml.py

+ 1 - 1
Pipfile

@@ -10,7 +10,7 @@ black = "==19.10b0"
 pylint = "*"
 pytest = "*"
 pytest-cov = "*"
-yamily = {extras = ["yaml"], path = "."}
+yamily = {editable = true,extras = ["yaml"],path = "."}
 
 [requires]
 python_version = "3"

+ 2 - 1
Pipfile.lock

@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "1aa10cf2b1025b851ea8a8bed6a9d3cc979995a8019419ec57b7af3165404887"
+            "sha256": "99d13a62fe09ae53c64b23fa80e842de2a905fa22db42580154679ef9896173d"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -300,6 +300,7 @@
             "version": "==1.11.2"
         },
         "yamily": {
+            "editable": true,
             "extras": [
                 "yaml"
             ],

+ 1 - 1
setup.py

@@ -24,7 +24,7 @@ setuptools.setup(
         "Topic :: Sociology :: Genealogy",
         "Topic :: Utilities",
     ],
-    # entry_points={"console_scripts": ["yamily = yamily._cli:main"]},
+    entry_points={"console_scripts": ["yamily-list = yamily._cli:_list"]},
     install_requires=[],
     extras_require={"yaml": ["PyYAML"],},
     setup_requires=["setuptools_scm"],

+ 86 - 0
tests/cli/_list_test.py

@@ -0,0 +1,86 @@
+import pathlib
+from unittest.mock import patch
+
+import pytest
+
+from yamily._cli import _list
+
+
+@patch("sys.argv", ["", "non/existing/file.yml"])
+def test__read_non_existing():
+    with pytest.raises(FileNotFoundError):
+        _list()
+
+
+@patch("sys.argv", ["", "persons/erika-mustermann.yml"])
+def test__list_single_simple(capsys):
+    _list()
+    out, err = capsys.readouterr()
+    assert not err
+    assert out == (
+        "- !person\n"
+        "  birth_date: 1957-08-12\n"
+        "  identifier: erika-mustermann\n"
+        "  name: Erika Mustermann\n"
+    )
+
+
+@patch("sys.argv", ["", "persons/max-mustermann.yml"])
+def test__list_single_parents(capsys):
+    _list()
+    out, err = capsys.readouterr()
+    assert not err
+    assert out == (
+        "- &id001 !person\n"
+        "  identifier: erika-mustermann\n"
+        "- !person\n"
+        "  birth_date: 1976-02-01\n"
+        "  father: &id002 !person\n"
+        "    identifier: thomas-mustermann\n"
+        "  identifier: max-mustermann\n"
+        "  mother: *id001\n"
+        "  name: Max Mustermann\n"
+        "- *id002\n"
+    )
+
+
+@patch("sys.argv", ["", "persons/max-mustermann.yml", "persons/erika-mustermann.yml"])
+def test__list_multiple(capsys):
+    _list()
+    out, err = capsys.readouterr()
+    assert not err
+    assert out == (
+        "- &id001 !person\n"
+        "  birth_date: 1957-08-12\n"
+        "  identifier: erika-mustermann\n"
+        "  name: Erika Mustermann\n"
+        "- !person\n"
+        "  birth_date: 1976-02-01\n"
+        "  father: &id002 !person\n"
+        "    identifier: thomas-mustermann\n"
+        "  identifier: max-mustermann\n"
+        "  mother: *id001\n"
+        "  name: Max Mustermann\n"
+        "- *id002\n"
+    )
+
+
+@patch("sys.argv", ["", "persons"])
+def test__list_recurse_dir(capsys):
+    _list()
+    out, err = capsys.readouterr()
+    assert not err
+    assert out == (
+        "- &id001 !person\n"
+        "  birth_date: 1957-08-12\n"
+        "  identifier: erika-mustermann\n"
+        "  name: Erika Mustermann\n"
+        "- !person\n"
+        "  birth_date: 1976-02-01\n"
+        "  father: &id002 !person\n"
+        "    identifier: thomas-mustermann\n"
+        "  identifier: max-mustermann\n"
+        "  mother: *id001\n"
+        "  name: Max Mustermann\n"
+        "- *id002\n"
+    )

+ 76 - 0
tests/person_collection.py

@@ -0,0 +1,76 @@
+import datetime
+
+from yamily import Person, PersonCollection
+
+
+def test_add_person_again():
+    collection = PersonCollection()
+    alice1 = Person("alice")
+    alice1.name = "Alice"
+    collection.add_person(alice1)
+    alice2 = Person("alice")
+    alice2.birth_date = datetime.date(2019, 12, 23)
+    collection.add_person(alice2)
+    assert len(list(collection)) == 1
+    assert collection["alice"].name == "Alice"
+    assert collection["alice"].birth_date == datetime.date(2019, 12, 23)
+
+
+def test_add_person_unknown_parents():
+    collection = PersonCollection()
+    alice = Person("alice")
+    alice.name = "Alice"
+    alice.birth_date = datetime.date(2019, 12, 23)
+    alice.mother = Person("mother")
+    alice.father = Person("father")
+    collection.add_person(alice)
+    assert collection["alice"].birth_date == datetime.date(2019, 12, 23)
+    assert collection["alice"] is alice
+    assert collection["mother"] is alice.mother
+    assert collection["father"] is alice.father
+
+
+def test_add_person_known_parents():
+    collection = PersonCollection()
+    mother = Person("mother")
+    mother.name = "Mum"
+    collection.add_person(mother)
+    collection.add_person(Person("father"))
+    alice = Person("alice")
+    alice.name = "Alice"
+    alice.birth_date = datetime.date(2019, 12, 23)
+    alice.mother = Person("mother")
+    alice.father = Person("father")
+    collection.add_person(alice)
+    assert collection["alice"].birth_date == datetime.date(2019, 12, 23)
+    assert collection["alice"] is alice
+    assert collection["mother"] is mother
+    assert collection["mother"] is alice.mother
+    assert collection["alice"].mother.name == "Mum"
+    assert collection["father"] is alice.father
+
+
+def test_add_person_later_parents():
+    collection = PersonCollection()
+    alice = Person("alice")
+    alice.name = "Alice"
+    alice.birth_date = datetime.date(2019, 12, 23)
+    alice.mother = Person("mother")
+    alice.father = Person("father")
+    collection.add_person(alice)
+    assert collection["mother"].name is None
+    assert collection["father"].name is None
+    mother = Person("mother")
+    mother.name = "Mum"
+    stored_mother = collection.add_person(mother)
+    father = Person("father")
+    father.name = "Dad"
+    stored_father = collection.add_person(father)
+    assert collection["alice"].birth_date == datetime.date(2019, 12, 23)
+    assert collection["alice"] is alice
+    assert collection["mother"] is alice.mother
+    assert collection["mother"] is stored_mother
+    assert collection["alice"].mother.name == "Mum"
+    assert collection["father"] is alice.father
+    assert collection["father"] is stored_father
+    assert collection["alice"].father.name == "Dad"

+ 2 - 2
yamily/__init__.py

@@ -132,9 +132,9 @@ class PersonCollection:
         [Person(alice), Person(bob, *2010-02-03), Person(bob-mum), Person(bob-dad)]
         """
         if person.mother is not None:
-            self.add_person(person.mother)
+            person.mother = self.add_person(person.mother)
         if person.father is not None:
-            self.add_person(person.father)
+            person.father = self.add_person(person.father)
         if person.identifier not in self._persons:
             self._persons[person.identifier] = person
             return person

+ 39 - 0
yamily/_cli.py

@@ -0,0 +1,39 @@
+import argparse
+import pathlib
+import sys
+import typing
+
+import yaml
+
+import yamily
+import yamily.yaml
+
+
+def _read(path: pathlib.Path) -> typing.Iterator[yamily.Person]:
+    if path.is_dir():
+        for file_path in path.glob("**/*.yml"):
+            for person in _read(file_path):
+                yield person
+    else:
+        yield yamily.yaml.read(path)
+
+
+def _list() -> None:
+    argparser = argparse.ArgumentParser(
+        description="Recursively find yamily family tree members and print as YAML list."
+    )
+    argparser.add_argument(
+        "paths", nargs="+", metavar="path", help="path to yamily .yml file or folder"
+    )
+    args = argparser.parse_args()
+    collection = yamily.PersonCollection()
+    for path in args.paths:
+        for person in _read(pathlib.Path(path)):
+            collection.add_person(person)
+    yaml.dump(
+        sorted(collection, key=lambda p: p.identifier),
+        sys.stdout,
+        Dumper=yamily.yaml.Dumper,
+        default_flow_style=False,
+        allow_unicode=True,
+    )

+ 50 - 5
yamily/yaml.py

@@ -1,3 +1,4 @@
+import datetime  # pylint: disable=unused-import; doctest
 import pathlib
 import typing
 
@@ -6,7 +7,7 @@ import yaml
 from yamily import Person
 
 
-class _YamlLoader(yaml.SafeLoader):
+class _Loader(yaml.SafeLoader):
 
     # pylint: disable=too-many-ancestors
 
@@ -15,9 +16,7 @@ class _YamlLoader(yaml.SafeLoader):
         self.add_constructor("!person", self._construct_person)
 
     @staticmethod
-    def _construct_person(
-        loader: "_YamlLoader", node: yaml.nodes.MappingNode
-    ) -> Person:
+    def _construct_person(loader: "_Loader", node: yaml.nodes.MappingNode) -> Person:
         (person_attrs,) = loader.construct_yaml_map(node)
         person = Person(person_attrs["identifier"])
         if "name" in person_attrs:
@@ -31,6 +30,52 @@ class _YamlLoader(yaml.SafeLoader):
         return person
 
 
+class Dumper(yaml.SafeDumper):
+
+    """
+    >>> p = Person('alice')
+    >>> p.name = 'Alice'
+    >>> p.birth_date = datetime.date(1976, 2, 1)
+    >>> print(yaml.dump(p, Dumper=Dumper))
+    !person
+    birth_date: 1976-02-01
+    identifier: alice
+    name: Alice
+    <BLANKLINE>
+
+    >>> p = Person('bob')
+    >>> p.mother = Person('bob-mum')
+    >>> p.father = Person('bob-father')
+    >>> print(yaml.dump(p, Dumper=Dumper))
+    !person
+    father: !person
+      identifier: bob-father
+    identifier: bob
+    mother: !person
+      identifier: bob-mum
+    <BLANKLINE>
+    """
+
+    # pylint: disable=too-many-ancestors
+
+    def __init__(self, stream, **kwargs):
+        super().__init__(stream, **kwargs)
+        self.add_representer(Person, self._represent_person)
+
+    @staticmethod
+    def _represent_person(dumper: "_Dumper", person: Person) -> yaml.nodes.MappingNode:
+        person_attrs = {"identifier": person.identifier}
+        if person.name is not None:
+            person_attrs["name"] = person.name
+        if person.birth_date is not None:
+            person_attrs["birth_date"] = person.birth_date
+        if person.mother is not None:
+            person_attrs["mother"] = person.mother
+        if person.father is not None:
+            person_attrs["father"] = person.father
+        return dumper.represent_mapping("!person", person_attrs)
+
+
 def read(yaml_path: typing.Union[str, pathlib.Path]) -> Person:
     """
     >>> read('persons/erika-mustermann.yml')
@@ -46,4 +91,4 @@ def read(yaml_path: typing.Union[str, pathlib.Path]) -> Person:
     if isinstance(yaml_path, str):
         return read(pathlib.Path(yaml_path))
     with yaml_path.open("r") as yaml_file:
-        return yaml.load(yaml_file, Loader=_YamlLoader)
+        return yaml.load(yaml_file, Loader=_Loader)