Skip to content

Commit

Permalink
Ensure TextInfo.getTextInChunks does not freeze, and provide new frie…
Browse files Browse the repository at this point in the history
…ndly compareable TextInfo endpoint properties (#12253)

TextInfo.getTextInChunks could sometimes end up in an infinit loop when dealing with particular TextInfo implementations such as ITextDocument where setting start to the end of the document results in the start never quite getting there.

To work around this specific freeze, getTextInChunks now double checks that the start has really moved to the end of the last chunk. If not, we break out of the loop.

However, for a long time now we have wanted to make these TextInfo comparisons much more readable, which can aide in finding logic errors much easier.
To that end, TextInfo objects now have start and end properties, which can be compared mathematically, and also set from another, removing the need to use compareEndPoints or setEndPoint.
Some examples:
Setting a's end to b's start
Original code:
a.setEndPoint(b, "endToStart")
New code:
a.end = b.start
Is a's start at or past b's end?
Original code:
a.compareEndPoints(b, "startToEnd") >= 0
New code:
a.start >= b.end
TextInfo.getTextInChunks has been rewritten to use these new properties.
  • Loading branch information
michaelDCurran authored Apr 6, 2021
1 parent ebe1ab5 commit 0efeee4
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 6 deletions.
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))
7 changes: 7 additions & 0 deletions user_docs/en/changes.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ What's New in NVDA
- Respect the GUI layout direction based on the NVDA language, not the system locale. (#638)
- known issue for right-to-left languages: the right border of groupings clips with labels/controls. (#12181)
- The python locale is set to match the language selected in preferences consistently, and will occur when using the default language. (#12214)
- - TextInfo.getTextInChunks no longer freezes when called on Rich Edit controls such as the NVDA log viewer. (#11613)


== Changes for Developers ==
Expand Down Expand Up @@ -100,6 +101,12 @@ What's New in NVDA
- Function winVersion.getWinVer has been added to get a winVersion.WinVersion representing the currently running OS.
- Convenience constants have been added for known Windows releases, see winVersion.WIN* constants.
- IAccessibleHandler no longer star imports everything from IAccessible and IA2 COM interfaces - please use them directly. (#12232)
- TextInfo objects now have start and end properties which can be compared mathematically with operators such as < <= == != >= >. (#11613)
- E.g. ti1.start <= ti2.end
- This usage is now prefered instead of ti1.compareEndPoints(ti2,"startToEnd") <= 0
- TextInfo start and end properties can also be set to each other.
- E.g. ti1.start = ti2.end
- This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd")


= 2020.4 =
Expand Down

0 comments on commit 0efeee4

Please sign in to comment.