Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions gedcom/element/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ class Element(object):
"""

def __init__(self, level, pointer, tag, value, crlf="\n", multi_line=True):
"""Creates a new GEDCOM element.
:type level: int
:type pointer: str
:type tag: str
:type value: str
:param crlf: line ending appended when serialising the element
:type crlf: str
:param multi_line: when True, long values are split across CONC/CONT continuation lines
:type multi_line: bool
"""
# basic element info
self.__level = level
self.__pointer = pointer
Expand Down Expand Up @@ -131,7 +141,8 @@ def __available_characters(self):
return 0 if element_characters > 255 else 255 - element_characters

def __line_length(self, line):
"""@TODO Write docs.
"""Returns the number of characters from `line` that fit within the available space of a GEDCOM line (max 255 chars).
Trims trailing spaces to avoid splitting a word across lines.
:type line: str
:rtype: int
"""
Expand All @@ -147,7 +158,8 @@ def __line_length(self, line):
return available_characters - spaces

def __set_bounded_value(self, value):
"""@TODO Write docs.
"""Sets this element's value to as many characters of `value` as fit within GEDCOM line limits.
Returns the number of characters consumed.
:type value: str
:rtype: int
"""
Expand All @@ -156,7 +168,8 @@ def __set_bounded_value(self, value):
return line_length

def __add_bounded_child(self, tag, value):
"""@TODO Write docs.
"""Adds a child element with the given `tag` and as many characters of `value` as fit within GEDCOM line limits.
Returns the number of characters consumed.
:type tag: str
:type value: str
:rtype: int
Expand All @@ -165,8 +178,8 @@ def __add_bounded_child(self, tag, value):
return child.__set_bounded_value(value)

def __add_concatenation(self, string):
"""@TODO Write docs.
:rtype: str
"""Splits `string` into GEDCOM CONC child elements, each fitting within line length limits.
:type string: str
"""
index = 0
size = len(string)
Expand Down
5 changes: 5 additions & 0 deletions gedcom/element/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@


class NotAnActualFamilyError(Exception):
"""Raised when an operation requires a FamilyElement but a different element type is provided."""
pass


class FamilyElement(Element):
"""Represents a GEDCOM family (FAM) record."""

def get_tag(self):
"""Returns the GEDCOM tag for this element.
:rtype: str
"""
return gedcom.tags.GEDCOM_TAG_FAMILY
5 changes: 5 additions & 0 deletions gedcom/element/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@


class NotAnActualFileError(Exception):
"""Raised when an operation requires a FileElement but a different element type is provided."""
pass


class FileElement(Element):
"""Represents a GEDCOM file (FILE) record."""

def get_tag(self):
"""Returns the GEDCOM tag for this element.
:rtype: str
"""
return gedcom.tags.GEDCOM_TAG_FILE
16 changes: 12 additions & 4 deletions gedcom/element/individual.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,17 @@


class NotAnActualIndividualError(Exception):
"""Raised when an operation requires an IndividualElement but a different element type is provided."""
pass


class IndividualElement(Element):
"""Represents a GEDCOM individual (INDI) record."""

def get_tag(self):
"""Returns the GEDCOM tag for this element.
:rtype: str
"""
return gedcom.tags.GEDCOM_TAG_INDIVIDUAL

def is_deceased(self):
Expand Down Expand Up @@ -119,6 +124,9 @@ def get_name(self):
return given_name, surname

def get_all_names(self):
"""Returns a list of all name values for this individual.
:rtype: list of str
"""
return [a.get_value() for a in self.get_child_elements() if a.get_tag() == gedcom.tags.GEDCOM_TAG_NAME]

def surname_match(self, surname_to_match):
Expand Down Expand Up @@ -182,7 +190,7 @@ def get_birth_data(self):
return date, place, sources

def get_birth_year(self):
"""Returns the birth year of a person in integer format
"""Returns the birth year of a person as an integer, or -1 if no birth date is set or the year cannot be parsed.
:rtype: int
"""
date = ""
Expand Down Expand Up @@ -222,7 +230,7 @@ def get_death_data(self):
return date, place, sources

def get_death_year(self):
"""Returns the death year of a person in integer format
"""Returns the death year of a person as an integer, or -1 if no death date is set or the year cannot be parsed.
:rtype: int
"""
date = ""
Expand All @@ -247,7 +255,7 @@ def get_burial(self):
::deprecated:: As of version 1.0.0 use `get_burial_data()` method instead
:rtype: tuple
"""
self.get_burial_data()
return self.get_burial_data()

def get_burial_data(self):
"""Returns the burial data of a person formatted as a tuple: (`str` date, `str´ place, `list` sources)
Expand Down Expand Up @@ -278,7 +286,7 @@ def get_census(self):
::deprecated:: As of version 1.0.0 use `get_census_data()` method instead
:rtype: list of tuple
"""
self.get_census_data()
return self.get_census_data()

def get_census_data(self):
"""Returns a list of censuses of an individual formatted as tuples: (`str` date, `str´ place, `list` sources)
Expand Down
2 changes: 2 additions & 0 deletions gedcom/element/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@


class NotAnActualObjectError(Exception):
"""Raised when an operation requires an ObjectElement but a different element type is provided."""
pass


class ObjectElement(Element):
"""Represents a GEDCOM multimedia object (OBJE) record."""

def is_object(self):
"""Checks if this element is an actual object
Expand Down
13 changes: 8 additions & 5 deletions gedcom/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@


class GedcomFormatViolationError(Exception):
pass
"""Raised when a GEDCOM format violation is encountered during parsing."""


class Parser(object):
Expand Down Expand Up @@ -280,7 +280,7 @@ def __build_list(self, element, element_list):
def get_marriages(self, individual):
"""Returns a list of marriages of an individual formatted as a tuple (`str` date, `str` place)
:type individual: IndividualElement
:rtype: tuple
:rtype: list of tuple
"""
marriages = []
if not isinstance(individual, IndividualElement):
Expand Down Expand Up @@ -448,10 +448,13 @@ def get_parents(self, individual, parent_type="ALL"):
return parents

def find_path_to_ancestor(self, descendant, ancestor, path=None):
"""Return path from descendant to ancestor
:rtype: object
"""Return the path of IndividualElements from descendant to ancestor, or None if no path exists.

:type descendant: IndividualElement
:type ancestor: IndividualElement
:rtype: list of IndividualElement or None
"""
if not isinstance(descendant, IndividualElement) and isinstance(ancestor, IndividualElement):
if not isinstance(descendant, IndividualElement) or not isinstance(ancestor, IndividualElement):
raise NotAnActualIndividualError(
"Operation only valid for elements with %s tag." % gedcom.tags.GEDCOM_TAG_INDIVIDUAL
)
Expand Down
52 changes: 52 additions & 0 deletions tests/element/test_individual.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,55 @@ def test_get_all_names():

all_names = element.get_all_names()
assert len(all_names) == 2


def test_deprecated_get_burial_returns_data():
"""Regression test: get_burial() is missing a return statement and returns None instead of the burial tuple."""
element = IndividualElement(level=0, pointer="@I1@", tag="INDI", value="")
burial = element.new_child_element(tag=gedcom.tags.GEDCOM_TAG_BURIAL, value="")
burial.new_child_element(tag=gedcom.tags.GEDCOM_TAG_DATE, value="1 JAN 2000")
burial.new_child_element(tag=gedcom.tags.GEDCOM_TAG_PLACE, value="Szczecinek")

result = element.get_burial()

assert result is not None, "get_burial() returned None — missing return statement bug"
assert result == ("1 JAN 2000", "Szczecinek", [])


def test_deprecated_get_census_returns_data():
"""Regression test: get_census() is missing a return statement and returns None instead of the census list."""
element = IndividualElement(level=0, pointer="@I1@", tag="INDI", value="")
census = element.new_child_element(tag=gedcom.tags.GEDCOM_TAG_CENSUS, value="")
census.new_child_element(tag=gedcom.tags.GEDCOM_TAG_DATE, value="1 JAN 1950")
census.new_child_element(tag=gedcom.tags.GEDCOM_TAG_PLACE, value="Szczebrzeszyn")

result = element.get_census()

assert result is not None, "get_census() returned None — missing return statement bug"
assert len(result) == 1
assert result[0] == ("1 JAN 1950", "Szczebrzeszyn", [])


def test_get_burial_data_returns_data():
element = IndividualElement(level=0, pointer="@I1@", tag="INDI", value="")
burial = element.new_child_element(tag=gedcom.tags.GEDCOM_TAG_BURIAL, value="")
burial.new_child_element(tag=gedcom.tags.GEDCOM_TAG_DATE, value="1 JAN 2000")
burial.new_child_element(tag=gedcom.tags.GEDCOM_TAG_PLACE, value="Szczecinek")

result = element.get_burial_data()

assert result is not None
assert result == ("1 JAN 2000", "Szczecinek", [])


def test_get_census_data_returns_data():
element = IndividualElement(level=0, pointer="@I1@", tag="INDI", value="")
census = element.new_child_element(tag=gedcom.tags.GEDCOM_TAG_CENSUS, value="")
census.new_child_element(tag=gedcom.tags.GEDCOM_TAG_DATE, value="1 JAN 1950")
census.new_child_element(tag=gedcom.tags.GEDCOM_TAG_PLACE, value="Szczebrzeszyn")

result = element.get_census_data()

assert result is not None
assert len(result) == 1
assert result[0] == ("1 JAN 1950", "Szczebrzeszyn", [])
32 changes: 31 additions & 1 deletion tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from gedcom.element.individual import IndividualElement
import pytest
from gedcom.element.element import Element
from gedcom.element.individual import IndividualElement, NotAnActualIndividualError
from gedcom.element.root import RootElement
from gedcom.parser import Parser

Expand Down Expand Up @@ -101,3 +103,31 @@ def test_parse_from_string():
def test___parse_line():
# @TODO Add appropriate testing cases
pass


def test_find_path_to_ancestor_raises_for_invalid_ancestor():
"""Regression test for the 'and' vs 'or' bug in find_path_to_ancestor.

The guard condition uses 'and' instead of 'or', so passing a valid
descendant with an invalid ancestor silently skips the check and crashes
later with an AttributeError instead of NotAnActualIndividualError.
"""
parser = Parser()
individual = IndividualElement(0, '@I1@', 'INDI', '', '\n')
non_individual = Element(0, '', 'NOTE', 'some note', '\n')

# valid descendant, invalid ancestor — the bug lets this slip through
with pytest.raises(NotAnActualIndividualError):
parser.find_path_to_ancestor(individual, non_individual)


def test_find_path_to_ancestor_raises_for_both_invalid():
"""Both arguments invalid — the buggy 'and' condition evaluates to False
and skips the guard entirely, crashing later instead of raising properly.
"""
parser = Parser()
non_individual_1 = Element(0, '', 'NOTE', 'some note', '\n')
non_individual_2 = Element(0, '', 'SOUR', 'some source', '\n')

with pytest.raises(NotAnActualIndividualError):
parser.find_path_to_ancestor(non_individual_1, non_individual_2)