Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure TextInfo.getTextInChunks does not freeze, and provide new friendly compareable TextInfo endpoint properties #12253

Merged
merged 6 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from 5 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
114 changes: 108 additions & 6 deletions source/textInfos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@
from abc import abstractmethod
import weakref
import re
from typing import Any, Union, List, Optional, Dict
from typing import (
Any,
Union,
List,
Optional,
Dict,
Tuple,
)

import baseObject
import config
import controlTypes
from controlTypes import OutputReason
import locationHelper
from logHandler import log


SpeechSequence = List[Union[Any, str]]
Expand Down Expand Up @@ -307,6 +315,24 @@ def __init__(self,obj,position):
#: The position with which this instance was constructed.
self.basePosition=position

#: Typing information for auto-property: start
start: "TextInfoEndpoint"

def _get_start(self) -> "TextInfoEndpoint":
return TextInfoEndpoint(self, True)

def _set_start(self, otherEndpoint: "TextInfoEndpoint"):
self.start.moveTo(otherEndpoint)

#: Typing information for auto-property: end
end: "TextInfoEndpoint"

def _get_end(self) -> "TextInfoEndpoint":
return TextInfoEndpoint(self, False)

def _set_end(self, otherEndpoint: "TextInfoEndpoint"):
self.end.moveTo(otherEndpoint)

def _get_obj(self):
"""The object containing the range of text being represented."""
return self._obj()
Expand Down Expand Up @@ -521,15 +547,18 @@ def getTextInChunks(self, unit):
"""
unitInfo=self.copy()
unitInfo.collapse()
while unitInfo.compareEndPoints(self,"startToEnd")<0:
while unitInfo.start < self.end:
unitInfo.expand(unit)
chunkInfo=unitInfo.copy()
if chunkInfo.compareEndPoints(self,"startToStart")<0:
chunkInfo.setEndPoint(self,"startToStart")
if chunkInfo.compareEndPoints(self,"endToEnd")>0:
chunkInfo.setEndPoint(self,"endToEnd")
if chunkInfo.start < self.start:
chunkInfo.start = self.start
if chunkInfo.end > self.end:
chunkInfo.end = self.end
yield chunkInfo.text
unitInfo.collapse(end=True)
if unitInfo.start < chunkInfo.end:
log.debugWarning("Could not move TextInfo completely to end, breaking")
break

def getControlFieldSpeech(
self,
Expand Down Expand Up @@ -624,3 +653,76 @@ def turnPage(self, previous=False):
@raise RuntimeError: If there are no further pages.
"""
raise NotImplementedError


class TextInfoEndpoint:
"""
Represents one end of a TextInfo instance.
This object can be compared with another end from the same or a different TextInfo instance,
Using the standard math comparison operators:
< <= == != >= >
"""

_whichMap: Dict[Tuple[bool, bool], str] = {
(True, True): "startToStart",
(True, False): "startToEnd",
(False, True): "endToStart",
(False, False): "endToEnd",
}

def _cmp(self, other: "TextInfoEndpoint") -> int:
"""
A standard cmp function returning:
-1 for less than, 0 for equal and 1 for greater than.
"""
if (
not isinstance(other, TextInfoEndpoint)
or not isinstance(other.textInfo, type(self.textInfo))
):
raise ValueError(f"Cannot compare endpoint with different type: {other}")
return self.textInfo.compareEndPoints(other.textInfo, self._whichMap[self.isStart, other.isStart])

def __init__(
self,
textInfo: TextInfo,
isStart: bool
):
"""
@param textInfo: the TextInfo instance you wish to represent an endpoint of.
@param isStart: true to represent the start, false for the end.
"""
self.textInfo = textInfo
self.isStart = isStart

def __lt__(self, other) -> bool:
return self._cmp(other) < 0

def __le__(self, other) -> bool:
return self._cmp(other) <= 0

def __eq__(self, other) -> bool:
return self._cmp(other) == 0

def __ne__(self, other) -> bool:
return self._cmp(other) != 0

def __ge__(self, other) -> bool:
return self._cmp(other) >= 0

def __gt__(self, other) -> bool:
return self._cmp(other) > 0

def moveTo(self, other: "TextInfoEndpoint") -> None:
"""
Moves the end of the TextInfo this endpoint represents to the position of the given endpoint.
"""
if (
not isinstance(other, TextInfoEndpoint)
or not isinstance(other.textInfo, type(self.textInfo))
):
raise ValueError(f"Cannot move endpoint to different type: {other}")
self.textInfo.setEndPoint(other.textInfo, self._whichMap[(self.isStart, other.isStart)])

def __repr__(self):
endpointLabel = "start" if self.isStart else "end"
return f"{endpointLabel} endpoint of {self.textInfo}"
42 changes: 42 additions & 0 deletions tests/unit/test_textInfos.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,45 @@ def test_mixedSurrogatePairsNonSurrogatesAndSingleSurrogatesBackward(self):
ti.move(textInfos.UNIT_CHARACTER, -1)
ti.expand(textInfos.UNIT_CHARACTER) # Range at a
self.assertEqual(ti.offsets, (0, 1)) # One offset


class TestEndpoints(unittest.TestCase):

def test_TextInfoEndpoint_largerAndSmaller(self):
obj = BasicTextProvider(text="abcdef")
ti = obj.makeTextInfo(Offsets(0, 2))
smaller = ti.start
larger = ti.end
self.assertTrue(smaller < larger)
self.assertFalse(larger < smaller)
self.assertTrue(smaller <= larger)
self.assertFalse(larger <= smaller)
self.assertFalse(smaller >= larger)
self.assertTrue(larger >= smaller)
self.assertFalse(smaller > larger)
self.assertTrue(larger > smaller)
self.assertTrue(smaller != larger)
self.assertTrue(larger != smaller)

def test_TextInfoEndpoint_equal(self):
obj = BasicTextProvider(text="abcdef")
ti = obj.makeTextInfo(Offsets(1, 1))
self.assertTrue(ti.start == ti.end)
self.assertFalse(ti.start != ti.end)
self.assertFalse(ti.start < ti.end)
self.assertTrue(ti.start <= ti.end)
self.assertTrue(ti.start >= ti.end)
self.assertFalse(ti.start > ti.end)

def test_setEndpoint(self):
obj = BasicTextProvider(text="abcdef")
ti1 = obj.makeTextInfo(Offsets(0, 2))
ti2 = obj.makeTextInfo(Offsets(3, 5))
ti1.end = ti2.end
self.assertEqual((ti1._startOffset, ti1._endOffset), (0, 5))
ti1.start = ti2.start
self.assertEqual((ti1._startOffset, ti1._endOffset), (3, 5))
ti1.end = ti2.start
self.assertEqual((ti1._startOffset, ti1._endOffset), (3, 3))
ti1.start = ti2.end
self.assertEqual((ti1._startOffset, ti1._endOffset), (5, 5))