From f478afe738ca3e39b8a306d9fea472c22cac57a3 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 18:47:36 -0500 Subject: [PATCH 01/13] update expected serialized format --- anchorpoint/textselectors.py | 4 +++- setup.py | 3 +++ tests/test_selectors.py | 2 +- tests/test_set.py | 2 +- tox.ini | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index 133188b..e29e401 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -10,7 +10,7 @@ import re from typing import List, Optional, Sequence, Tuple, Union - +from pydantic import ValidationError from anchorpoint.textsequences import TextPassage, TextSequence from ranges import Range, RangeSet, Inf from ranges._helper import _InfiniteValue @@ -292,6 +292,8 @@ def start_not_negative(cls, v) -> bool: """ if v < 0: raise IndexError("Start position for text range cannot be negative.") + elif v is None: + raise ValidationError("Start position for text range cannot be negative.") return v @validator("end") diff --git a/setup.py b/setup.py index 7092827..524158e 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,9 @@ "Development Status :: 4 - Beta", "License :: Free To Use But Restricted", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Operating System :: OS Independent", "Natural Language :: English", ], diff --git a/tests/test_selectors.py b/tests/test_selectors.py index e869406..b0015cd 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -33,7 +33,7 @@ class TestTextQuoteSelectors: def test_convert_selector_to_json(self): copyright_json = self.preexisting_material.json() - assert '"exact": "protection for a work' in copyright_json + assert '"exact":"protection for a work' in copyright_json def test_create_from_text(self): method = TextQuoteSelector.from_text( diff --git a/tests/test_set.py b/tests/test_set.py index 36776dd..53baacd 100644 --- a/tests/test_set.py +++ b/tests/test_set.py @@ -405,7 +405,7 @@ def test_serialize_set_with_pydantic(self): def test_get_schema_with_pydantic(self): assert ( TextPositionSet.schema()["properties"]["positions"]["items"]["$ref"] - == "#/definitions/TextPositionSelector" + == "#/$defs/TextPositionSelector" ) def test_set_as_text_sequence_with_no_endpoint(self): diff --git a/tox.ini b/tox.ini index 30365fe..45d39bd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # content of: tox.ini , put in same dir as setup.py [tox] -envlist = py310, py39, py38 +envlist = py312, py311, py310, py39, py38 [testenv] # install testing framework # ... or install anything else you might need here From d0991f6dc219bd3213570b9e36ef477d7494e08e Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 18:54:25 -0500 Subject: [PATCH 02/13] use model_validator to check start less than end --- anchorpoint/textselectors.py | 11 +++++------ anchorpoint/textsequences.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index e29e401..01ac363 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -14,7 +14,7 @@ from anchorpoint.textsequences import TextPassage, TextSequence from ranges import Range, RangeSet, Inf from ranges._helper import _InfiniteValue -from pydantic import BaseModel, validator +from pydantic import BaseModel, validator, model_validator class TextSelectionError(Exception): @@ -296,18 +296,17 @@ def start_not_negative(cls, v) -> bool: raise ValidationError("Start position for text range cannot be negative.") return v - @validator("end") - def start_less_than_end(cls, v, values): + @model_validator(mode="after") + def start_less_than_end(self) -> "TextPositionSelector": """ Verify start position is before the end position. :returns: the end position, which after the start position """ - start, end = values.get("start"), v - if end is not None and end <= start: + if self.end is not None and self.end <= self.start: raise IndexError("End position must be greater than start position.") - return v + return self def range(self) -> Range: """Get the range of the text.""" diff --git a/anchorpoint/textsequences.py b/anchorpoint/textsequences.py index e19e1c4..533f06b 100644 --- a/anchorpoint/textsequences.py +++ b/anchorpoint/textsequences.py @@ -74,7 +74,7 @@ class TextSequence(Sequence[Union[None, TextPassage]]): def __init__(self, passages: List[Optional[TextPassage]] = None): """ Make new TextSequence from :class:`.TextPassage` list. - + :param passages: the text passages included in the TextSequence, which should be chosen to express a coherent idea. "None"s in the sequence represent spans of From 79e71ac78678ad45ff41210df0a3739afdcb0b42 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 19:14:56 -0500 Subject: [PATCH 03/13] change to pydantic v2-style validators --- anchorpoint/textselectors.py | 7 ++++--- docs/guides/selecting.rst | 4 ++-- tests/test_schema.py | 6 +++--- tests/test_selectors.py | 2 +- tests/test_set.py | 4 ++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index 01ac363..2fc7bde 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -14,7 +14,7 @@ from anchorpoint.textsequences import TextPassage, TextSequence from ranges import Range, RangeSet, Inf from ranges._helper import _InfiniteValue -from pydantic import BaseModel, validator, model_validator +from pydantic import BaseModel, field_validator, validator, model_validator class TextSelectionError(Exception): @@ -69,8 +69,9 @@ def split_anchor_text(text: str) -> Tuple[str, ...]: "two, separating the string into 'prefix', 'exact', and 'suffix'." ) - @validator("prefix", "exact", "suffix", pre=True) - def no_none_for_prefix(cls, value): + @field_validator("prefix", "exact", "suffix", mode="before") + @classmethod + def no_none_for_prefix(cls, value: str | None) -> str: """Ensure that 'prefix', 'exact', and 'suffix' are not None.""" if value is None: return "" diff --git a/docs/guides/selecting.rst b/docs/guides/selecting.rst index d162cff..b06dd61 100644 --- a/docs/guides/selecting.rst +++ b/docs/guides/selecting.rst @@ -152,9 +152,9 @@ Anchorpoint uses `Pydantic `__ to serialize selectors either to Python dictionaries or to JSON strings suitable for sending over the internet with APIs. - >>> authorship_selector.json() + >>> authorship_selector.model_dump_json() '{"exact": "authorship", "prefix": "", "suffix": "include"}' - >>> selector_set.dict() + >>> selector_set.model_dump() {'positions': [{'start': 65, 'end': 79}, {'start': 100, 'end': 136}], 'quotes': []} Pydantic's data loading methods mean that you can also create the data for an diff --git a/tests/test_schema.py b/tests/test_schema.py index cb6e9e3..116328e 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -40,7 +40,7 @@ def test_ordered_position_selector_fields(self): """Test that "start" is before "end".""" data = {"start": 0, "end": 12} loaded = TextPositionSelector(**data) - dumped = loaded.dict() + dumped = loaded.model_dump() assert list(dumped.keys())[0] == "start" @@ -69,7 +69,7 @@ class TestDumpSelector: def test_dump_quote_selector(self): data = "eats,|shoots,|and leaves" loaded = TextQuoteSelector.from_text(data) - dumped = loaded.dict() + dumped = loaded.model_dump() assert dumped["prefix"] == "eats," assert dumped["suffix"] == "and leaves" @@ -77,6 +77,6 @@ def test_ordered_position_selector_fields(self): """Test that "start" is before "end".""" data = {"start": 0, "end": 12} loaded = TextPositionSelector(**data) - dumped = loaded.dict() + dumped = loaded.model_dump() assert dumped == {"start": 0, "end": 12} assert list(dumped.keys())[0] == "start" diff --git a/tests/test_selectors.py b/tests/test_selectors.py index b0015cd..a9602c6 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -32,7 +32,7 @@ class TestTextQuoteSelectors: ) def test_convert_selector_to_json(self): - copyright_json = self.preexisting_material.json() + copyright_json = self.preexisting_material.model_dump_json() assert '"exact":"protection for a work' in copyright_json def test_create_from_text(self): diff --git a/tests/test_set.py b/tests/test_set.py index 53baacd..656c30a 100644 --- a/tests/test_set.py +++ b/tests/test_set.py @@ -400,11 +400,11 @@ def test_serialize_set_with_pydantic(self): TextPositionSelector(start=5, end=10), ] ) - assert selector_set.dict()["positions"][0]["end"] == 4 + assert selector_set.model_dump()["positions"][0]["end"] == 4 def test_get_schema_with_pydantic(self): assert ( - TextPositionSet.schema()["properties"]["positions"]["items"]["$ref"] + TextPositionSet.model_json_schema()["properties"]["positions"]["items"]["$ref"] == "#/$defs/TextPositionSelector" ) From 42bf69d0798df4bf51dfeffc15fc09d0ee994a05 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 19:24:31 -0500 Subject: [PATCH 04/13] combine selector list validators --- anchorpoint/textselectors.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index 2fc7bde..8091342 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -669,14 +669,6 @@ def __sub__( new.quotes = self.quotes return new - @validator("positions", pre=True) - def selectors_are_in_list( - cls, selectors: Union[TextPositionSelector, List[TextPositionSelector]] - ): - """Put single selector in list.""" - if not isinstance(selectors, Sequence): - selectors = [selectors] - return selectors @validator("quotes", pre=True) def quote_selectors_are_in_list( @@ -695,9 +687,12 @@ def quote_selectors_are_in_list( ] return selectors - @validator("positions") - def order_of_selectors(cls, v): + @field_validator("positions", mode="before") + @classmethod + def order_of_selectors(cls, v: list[TextPositionSelector]): """Ensure that selectors are in order.""" + if not isinstance(v, Sequence): + v = [v] return sorted(v, key=lambda x: x.start) def positions_as_quotes(self, text: str) -> List[TextQuoteSelector]: From e0143fe8ce287db9c345fc545fbb233b157fe24f Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 19:31:16 -0500 Subject: [PATCH 05/13] use pydantic v2 field_validator for start_not_negative --- anchorpoint/textselectors.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index 8091342..d763c0e 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -283,18 +283,19 @@ def from_range( end = range.end return TextPositionSelector(start=range.start, end=end) - @validator("start") - def start_not_negative(cls, v) -> bool: + @field_validator("start", mode="after") + @classmethod + def start_not_negative(cls, v: int | None) -> int: """ Verify start position is not negative. :returns: the start position, which is not negative """ - if v < 0: + if v is None: + raise IndexError("Start position for text range cannot be None.") + elif v < 0: raise IndexError("Start position for text range cannot be negative.") - elif v is None: - raise ValidationError("Start position for text range cannot be negative.") return v @model_validator(mode="after") From f03092a4470b544f5d85355cc102c5368730b3f1 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 19:32:48 -0500 Subject: [PATCH 06/13] field_validator for quote_selectors_are_in_list --- anchorpoint/textselectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index d763c0e..7c0dba2 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -671,7 +671,7 @@ def __sub__( return new - @validator("quotes", pre=True) + @field_validator("quotes", mode="before") def quote_selectors_are_in_list( cls, selectors: Union[str, TextQuoteSelector, List[Union[str, TextQuoteSelector]]], From d45ea5cdbe36a7c2c72d8bdad29495911adc4017 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 19:37:29 -0500 Subject: [PATCH 07/13] remove unreachable validation check the type checker catches the None first --- anchorpoint/textselectors.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index 7c0dba2..40196f2 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -285,16 +285,14 @@ def from_range( @field_validator("start", mode="after") @classmethod - def start_not_negative(cls, v: int | None) -> int: + def start_not_negative(cls, v: int) -> int: """ Verify start position is not negative. :returns: the start position, which is not negative """ - if v is None: - raise IndexError("Start position for text range cannot be None.") - elif v < 0: + if v < 0: raise IndexError("Start position for text range cannot be negative.") return v From b6739280f0ff0831fd1b20f9ff8f540e1cbfae6c Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 19:50:51 -0500 Subject: [PATCH 08/13] use ruff formatter --- anchorpoint/textselectors.py | 4 +--- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index 40196f2..61be1bc 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -10,11 +10,10 @@ import re from typing import List, Optional, Sequence, Tuple, Union -from pydantic import ValidationError from anchorpoint.textsequences import TextPassage, TextSequence from ranges import Range, RangeSet, Inf from ranges._helper import _InfiniteValue -from pydantic import BaseModel, field_validator, validator, model_validator +from pydantic import BaseModel, field_validator, model_validator class TextSelectionError(Exception): @@ -668,7 +667,6 @@ def __sub__( new.quotes = self.quotes return new - @field_validator("quotes", mode="before") def quote_selectors_are_in_list( cls, diff --git a/requirements-dev.txt b/requirements-dev.txt index 77455ee..9c4357b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,11 +2,11 @@ black build coverage coveralls -flake8 mypy pydocstyle pytest pytest-cov +ruff rstcheck setuptools Sphinx From a5fd82d9bf2cdef6f388c1ef1c8b566603b35b5d Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 22:54:59 -0500 Subject: [PATCH 09/13] use github CI --- .circleci/config.yml | 48 ---------------------------- .github/workflows/python_package.yml | 37 +++++++++++++++++++++ requirements.txt | 4 +-- setup.py | 3 +- 4 files changed, 41 insertions(+), 51 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/python_package.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 446fb90..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,48 +0,0 @@ -# Python CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-python/ for more details -# -version: 2 -jobs: - build: - docker: - # specify the version you desire here - # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` - - image: cimg/python:3.10.0 - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - # - image: circleci/postgres:9.4 - - steps: - - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: - name: install dependencies - command: | - python3 -m venv venv - . venv/bin/activate - pip install -r requirements.txt - pip install -r requirements-dev.txt - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - - # run tests! - - run: - name: run tests - command: | - . venv/bin/activate - pytest --cov-report term --cov=anchorpoint tests/ - coveralls - - store_artifacts: - path: htmlcov diff --git a/.github/workflows/python_package.yml b/.github/workflows/python_package.yml new file mode 100644 index 0000000..2ee13db --- /dev/null +++ b/.github/workflows/python_package.yml @@ -0,0 +1,37 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest coveralls==2.1.2 pytest-cov + - name: Test with pytest + run: | + pytest tests/ --cov=anchorpoint --cov-report=term-missing + - name: Upload coverage data to coveralls.io + if: matrix.python-version == 3.12 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: coveralls \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3f7af21..61a463d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -pydantic>=1.8.2 -python-ranges>=0.2.1 +pydantic>=2.4.2 +python-ranges>=1.2.2 diff --git a/setup.py b/setup.py index 524158e..0765429 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ classifiers=[ "Development Status :: 4 - Beta", "License :: Free To Use But Restricted", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -27,6 +28,6 @@ "Natural Language :: English", ], packages=setuptools.find_packages(exclude=["tests"]), - install_requires=["pydantic>=1.8.2"], + install_requires=["pydantic>2.4.2", "python-ranges>=1.2.2"], python_requires=">=3.8", ) From cebc4f6cecb13d73ce7968781e99628b5c44a583 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 23:03:08 -0500 Subject: [PATCH 10/13] replace CircleCI readme badge --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7e74ec7..a76a4f5 100644 --- a/README.rst +++ b/README.rst @@ -11,9 +11,9 @@ A Python library for anchoring annotations with text substring selectors. :target: https://coveralls.io/github/mscarey/anchorpoint?branch=master :alt: Test Coverage Percentage -.. image:: https://circleci.com/gh/mscarey/anchorpoint.svg?style=svg - :target: https://circleci.com/gh/mscarey/anchorpoint - :alt: CircleCI Status +.. image:: https://github.com/mscarey/anchorpoint/actions/workflows/python-package.yml/badge.svg + :target: https://github.com/mscarey/anchorpoint/actions + :alt: GitHub Actions Workflow .. image:: https://readthedocs.org/projects/anchorpoint/badge/?version=latest :target: https://anchorpoint.readthedocs.io/en/latest/?badge=latest From 2a8033c6b88dc6a99cbc655ba14a5684508fe2e1 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 23:20:41 -0500 Subject: [PATCH 11/13] add type hints to from_quotes --- anchorpoint/textselectors.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index 61be1bc..f4655fd 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -399,7 +399,7 @@ def __and__( if isinstance(other, (TextPositionSelector, TextPositionSet)): other = other.rangeset() - new_rangeset = self.rangeset() & other + new_rangeset: RangeSet = self.rangeset() & other if not new_rangeset: return None @@ -552,7 +552,7 @@ class TextPositionSet(BaseModel): @classmethod def from_quotes( - self, + cls, selection: Union[str, TextQuoteSelector, List[Union[TextQuoteSelector, str]]], ) -> TextPositionSet: """ @@ -562,11 +562,11 @@ def from_quotes( """ if isinstance(selection, (str, TextQuoteSelector)): selection = [selection] - selection = [ + selection_as_selectors: list[TextQuoteSelector] = [ TextQuoteSelector.from_text(s) if isinstance(s, str) else s for s in selection ] - return TextPositionSet(quotes=selection) + return TextPositionSet(quotes=selection_as_selectors) @classmethod def from_ranges( From 924cbaad5f9ca4a833ac4f32f46c237374328295 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 23:28:24 -0500 Subject: [PATCH 12/13] fix type issue with TextPositionSet.sub() --- anchorpoint/textselectors.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index f4655fd..0aaf83f 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -650,7 +650,7 @@ def __and__( ) -> TextPositionSet: if isinstance(other, TextPositionSelector): other = TextPositionSet(positions=[other]) - new_rangeset = self.rangeset() & other.rangeset() + new_rangeset: RangeSet = self.rangeset() & other.rangeset() return TextPositionSet.from_ranges(new_rangeset) def __sub__( @@ -659,11 +659,12 @@ def __sub__( """Decrease all startpoints and endpoints by the given amount.""" if not isinstance(value, int): new_rangeset = self.rangeset() - value.rangeset() + new = TextPositionSet.from_ranges(new_rangeset) else: - new_rangeset = [ + new_selectors = [ selector.subtract_integer(value) for selector in self.positions ] - new = TextPositionSet.from_ranges(new_rangeset) + new = TextPositionSet.from_ranges(new_selectors) new.quotes = self.quotes return new From 7d50ff599a0c6fa1b03251ea9746f608e8b57b87 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Sun, 29 Oct 2023 23:43:44 -0500 Subject: [PATCH 13/13] add None checks for type checker --- anchorpoint/textselectors.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/anchorpoint/textselectors.py b/anchorpoint/textselectors.py index 0aaf83f..ad07396 100644 --- a/anchorpoint/textselectors.py +++ b/anchorpoint/textselectors.py @@ -736,7 +736,11 @@ def as_text_sequence(self, text: str, include_nones: bool = True) -> TextSequenc if include_nones and 0 < selection_ranges[0].start < len(text): selected.append(None) for passage in selection_ranges: - if passage.start < passage.end and passage.start < len(text): + if ( + passage.start is not None + and passage.start < passage.end + and passage.start < len(text) + ): string_end = ( passage.end if not isinstance(passage.end, _InfiniteValue) @@ -829,7 +833,10 @@ def add_margin( margin_selectors = TextPositionSet() for left in new_rangeset.ranges(): for right in new_rangeset.ranges(): - if left.end < right.start <= left.end + margin_width: + if ( + left.end is not None + and left.end < right.start <= left.end + margin_width + ): if all( letter in margin_characters for letter in text[left.end : right.start]