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

Update models to pydantic v2 #11

Merged
merged 13 commits into from
Oct 30, 2023
48 changes: 0 additions & 48 deletions .circleci/config.yml

This file was deleted.

37 changes: 37 additions & 0 deletions .github/workflows/python_package.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 34 additions & 32 deletions anchorpoint/textselectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
import re

from typing import List, Optional, Sequence, Tuple, Union

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, field_validator, model_validator


class TextSelectionError(Exception):
Expand Down Expand Up @@ -69,8 +68,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 ""
Expand Down Expand Up @@ -282,8 +282,9 @@ 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) -> int:
"""
Verify start position is not negative.

Expand All @@ -294,18 +295,17 @@ def start_not_negative(cls, v) -> bool:
raise IndexError("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."""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -552,7 +552,7 @@ class TextPositionSet(BaseModel):

@classmethod
def from_quotes(
self,
cls,
selection: Union[str, TextQuoteSelector, List[Union[TextQuoteSelector, str]]],
) -> TextPositionSet:
"""
Expand All @@ -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(
Expand Down Expand Up @@ -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__(
Expand All @@ -659,24 +659,16 @@ 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

@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)
@field_validator("quotes", mode="before")
def quote_selectors_are_in_list(
cls,
selectors: Union[str, TextQuoteSelector, List[Union[str, TextQuoteSelector]]],
Expand All @@ -693,9 +685,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]:
Expand Down Expand Up @@ -741,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)
Expand Down Expand Up @@ -834,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]
Expand Down
2 changes: 1 addition & 1 deletion anchorpoint/textsequences.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/selecting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ Anchorpoint uses `Pydantic <https://pydantic-docs.helpmanual.io/>`__ 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
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ black
build
coverage
coveralls
flake8
mypy
pydocstyle
pytest
pytest-cov
ruff
rstcheck
setuptools
Sphinx
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pydantic>=1.8.2
python-ranges>=0.2.1
pydantic>=2.4.2
python-ranges>=1.2.2
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@
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",
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
"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",
)
6 changes: 3 additions & 3 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -69,14 +69,14 @@ 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"

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"
4 changes: 2 additions & 2 deletions tests/test_selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ class TestTextQuoteSelectors:
)

def test_convert_selector_to_json(self):
copyright_json = self.preexisting_material.json()
assert '"exact": "protection for a work' in copyright_json
copyright_json = self.preexisting_material.model_dump_json()
assert '"exact":"protection for a work' in copyright_json

def test_create_from_text(self):
method = TextQuoteSelector.from_text(
Expand Down
6 changes: 3 additions & 3 deletions tests/test_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,12 +400,12 @@ 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"]
== "#/definitions/TextPositionSelector"
TextPositionSet.model_json_schema()["properties"]["positions"]["items"]["$ref"]
== "#/$defs/TextPositionSelector"
)

def test_set_as_text_sequence_with_no_endpoint(self):
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading