Skip to content
Merged
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
12 changes: 11 additions & 1 deletion dapytains/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,14 @@ class UnknownTreeName(Exception):
"""This exception is raised when a requested tree is unknown """

class InvalidRangeOrder(Exception):
"""Error raised when a range is in the wrong order (start > end) """
"""Error raised when a range is in the wrong order (start > end) """


class UnresolvableReference(ValueError):
"""Error raised when a reference matches no node of the document: it does not exist, or one of
its units only exists under a later occurrence of a repeated milestone parent (e.g. a column the
text comes back to), which resolution, following the first occurrence, cannot reach. """

def __init__(self, reference: str):
self.reference = reference
super().__init__(f"Reference '{reference}' cannot be resolved to a node of the document.")
3 changes: 3 additions & 0 deletions dapytains/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ def get_xpath_proc(elem: saxonlib.PyXdmNode, processor: saxonlib.PySaxonProcesso
:param elem: An XML node, root or not
:return: XPathProccesor
"""
if elem is None:
# Saxon segfaults on a None context instead of raising: fail in Python instead.
raise TypeError("get_xpath_proc() needs a node as context, got None")
xpath = processor.new_xpath_processor()
xpath.declare_namespace("", "http://www.tei-c.org/ns/1.0")
xpath.set_context(xdm_item=elem)
Expand Down
14 changes: 11 additions & 3 deletions dapytains/tei/citeStructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from collections import namedtuple, defaultdict
from dapytains.errors import UnresolvableReference
from dapytains.processor import get_xpath_proc, saxonlib

_pos_re = re.compile(r'\[(\d+)\]')
Expand Down Expand Up @@ -273,17 +274,21 @@ def _resolve_groups(self, groups: List[tuple]) -> Optional[saxonlib.PyXdmNode]:
xpath_proc = get_xpath_proc(self.doc_root, processor=self.processor)
concrete_node = None
prev_structure: Optional[CitableStructure] = None
for key, value in groups:
for position, (key, value) in enumerate(groups):
formatted = self.xpath_matcher[key].format(**{key: value})
structure = self.structure_by_key[key]
if concrete_node is None:
if position == 0:
concrete_node = xpath_proc.evaluate_single(f"({formatted})[1]")
elif prev_structure.milestone:
boundary = self._milestone_boundary(concrete_node, prev_structure.match)
concrete_node = (self._milestone_window(concrete_node, boundary, formatted) or [None])[0]
else:
local_proc = get_xpath_proc(concrete_node, processor=self.processor)
concrete_node = local_proc.evaluate_single(_relative("./", formatted))
if concrete_node is None:
# A missing unit: stop here, instead of looking for its children in the whole
# document (which would resolve "9.A" to the first letter A when there is no column 9).
return None
prev_structure = structure
return concrete_node

Expand Down Expand Up @@ -321,7 +326,10 @@ def generate_xpath(self, reference):
# (self-closing elements like <cb/>) cannot be joined with their children via "/", then
# turn it into its real, DOM-accurate absolute positional XPath so it flows through the
# rest of the pipeline (document.py's reconstruct_doc) like any other absolute XPath.
return self._absolute_path(self.resolve_node(reference))
node = self.resolve_node(reference)
if node is None:
raise UnresolvableReference(reference)
return self._absolute_path(node)

def get_next_milestone_boundary(self, reference: str) -> Optional[saxonlib.PyXdmNode]:
""" If `reference`'s deepest unit is nested directly under a milestone-mode parent
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def read_requirements():

setup(
name="dapytains",
version="1.0.0rc3",
version="1.0.0rc4",
author="Thibault Clérice",
author_email="thibault.clerice@inria.fr",
description="A brief description of dapytains",
Expand Down
24 changes: 24 additions & 0 deletions tests/tei/cb_repeated_column.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<TEI xmlns="http://www.tei-c.org/ns/1.0">
<teiHeader>
<encodingDesc>
<refsDecl>
<citeStructure unit="column" match="//body//cb" use="@n">
<citeStructure unit="letter" match=".//milestone[@unit='letter']" use="@n" delim="."/>
</citeStructure>
</refsDecl>
</encodingDesc>
</teiHeader>
<text>
<body>
<div>
<!-- The text comes back to column 1 twice (e.g. letters printed out of order in the
Patrologia): letter B only exists under the later occurrences of column 1. -->
<p><cb n="1"/><milestone unit="letter" n="A"/>Column one, A.</p>
<p><cb n="2"/><milestone unit="letter" n="A"/>Column two, A.</p>
<p><cb n="1"/><milestone unit="letter" n="B"/>Column one again, B.</p>
<p><cb n="3"/><milestone unit="letter" n="A"/>Column three, A.</p>
<p><cb n="1"/><milestone unit="letter" n="B"/>Column one a third time, B.</p>
</div>
</body>
</text>
</TEI>
51 changes: 51 additions & 0 deletions tests/test_unresolvable_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os.path

import pytest

from dapytains.errors import UnresolvableReference
from dapytains.processor import get_processor, get_xpath_proc
from dapytains.tei.document import Document

p = os.path.dirname(os.path.abspath(__file__))


@pytest.fixture
def doc():
# Column 1 comes back twice: letter B only exists under its later occurrences.
return Document(os.path.join(p, "tei/cb_repeated_column.xml"))


def test_resolvable_reference_still_works(doc):
assert doc.citeStructure[doc.default_tree].generate_xpath("1.A") == "/TEI[1]/text[1]/body[1]/div[1]/p[1]/milestone[1]"
assert doc.citeStructure[doc.default_tree].generate_xpath("2.A") == "/TEI[1]/text[1]/body[1]/div[1]/p[2]/milestone[1]"


def test_child_of_a_later_occurrence_raises(doc):
""" Used to hand None to Saxon as an XPath context, which segfaulted. """
with pytest.raises(UnresolvableReference) as error:
doc.citeStructure[doc.default_tree].generate_xpath("1.B")
assert error.value.reference == "1.B"


def test_missing_parent_raises_instead_of_matching_elsewhere(doc):
""" There is no column 9: "9.A" used to resolve to the first letter A of the document. """
assert doc.citeStructure[doc.default_tree].resolve_node("9.A") is None
with pytest.raises(UnresolvableReference):
doc.citeStructure[doc.default_tree].generate_xpath("9.A")


def test_get_passage_raises(doc):
with pytest.raises(UnresolvableReference):
doc.get_passage("1.B")
with pytest.raises(UnresolvableReference):
doc.get_passage("9.A")


def test_unresolvable_reference_is_a_value_error():
""" Callers catching ValueError (e.g. malformed references) keep working. """
assert issubclass(UnresolvableReference, ValueError)


def test_get_xpath_proc_refuses_none():
with pytest.raises(TypeError):
get_xpath_proc(None, processor=get_processor())
Loading