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
5 changes: 3 additions & 2 deletions src/cassis/cas.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections import defaultdict
from functools import lru_cache
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple, Union
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast

import attr
import deprecation
Expand All @@ -29,6 +29,7 @@
TYPE_NAME_NON_EMPTY_STRING_LIST,
TYPE_NAME_SOFA,
TYPE_NAME_STRING_LIST,
AnnotationBase,
FeatureStructure,
Annotation,
Type,
Expand Down Expand Up @@ -391,7 +392,7 @@ def add(self, fs: FeatureStructure, keep_id: Optional[bool] = True):

fs.xmiID = next_id
if hasattr(fs, "sofa"):
fs.sofa = self.get_sofa()
cast(AnnotationBase, fs).sofa = self.get_sofa()

self._current_view.add_fs_to_indexes(fs)

Expand Down
5 changes: 3 additions & 2 deletions src/cassis/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections import OrderedDict, defaultdict
from io import TextIOBase, TextIOWrapper
from math import isnan
from typing import Any, Union, IO, Optional, Dict
from typing import Any, Union, IO, Optional, Dict, cast
from toposort import toposort_flatten

from cassis.cas import NAME_DEFAULT_SOFA, Cas, IdGenerator, Sofa, View
Expand Down Expand Up @@ -36,6 +36,7 @@
element_type_name_for_array_type,
is_primitive,
is_array,
AnnotationBase,
)

RESERVED_FIELD_PREFIX = "%"
Expand Down Expand Up @@ -305,7 +306,7 @@ def fix_up(elements):

# Map from offsets in UIMA UTF-16 based offsets to Unicode codepoints
if typesystem.is_instance_of(fs.type, TYPE_NAME_ANNOTATION):
sofa = fs.sofa
sofa = cast(Sofa, cast(AnnotationBase, fs).sofa)
fs.begin = sofa._offset_converter.external_to_python(fs.begin)
fs.end = sofa._offset_converter.external_to_python(fs.end)

Expand Down
60 changes: 46 additions & 14 deletions src/cassis/typesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
from io import BytesIO
from itertools import chain, filterfalse
from pathlib import Path
from typing import IO, Any, Callable, Dict, Iterator, List, Optional, Set, TypeGuard, Union
from typing import IO, TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Set, TypeGuard, Union, cast

import attr
from deprecation import deprecated
from lxml import etree
from more_itertools import unique_everseen
from toposort import toposort_flatten

if TYPE_CHECKING:
from cassis.cas import Sofa

TOP_TYPE_NAME = "uima.cas.TOP"

NAMESPACE_SEPARATOR = "."
Expand Down Expand Up @@ -413,13 +416,14 @@ def get_covered_text(self) -> Optional[str]:

"""
if hasattr(self, "sofa") and hasattr(self, "begin") and hasattr(self, "end"):
if self.sofa is None:
this = cast("Annotation", self)
Comment thread
reckart marked this conversation as resolved.
if this.sofa is None:
raise AnnotationHasNoSofa(
"Annotations must have a SofA (be added to a CAS) before get_covered_text() can be called"
)
if self.sofa.sofaString is None:
if this.sofa.sofaString is None:
return None
return self.sofa.sofaString[self.begin : self.end]
return this.sofa.sofaString[this.begin : this.end]
else:
raise NotImplementedError()

Expand Down Expand Up @@ -501,7 +505,26 @@ def __repr__(self):


@attr.s(slots=True, eq=False, order=False, repr=False)
class Annotation(FeatureStructure):
class AnnotationBase(FeatureStructure):
"""Concrete base class for `uima.cas.AnnotationBase` feature structures.

Generated types that are (transitively) subtypes of `uima.cas.AnnotationBase`
inherit from this class so that static typing can rely on a nominal base
providing `sofa`. The `sofa` is conceptually mandatory for an annotation that
lives in a committed CAS, but is `None` during the window between constructing
the feature structure and adding it to a view (see `View.add`), so it is typed
as Optional.
"""

sofa: Optional["Sofa"] = attr.ib(default=None, repr=False)


def is_annotation_base(fs: FeatureStructure) -> TypeGuard[AnnotationBase]:
return isinstance(fs, AnnotationBase)


@attr.s(slots=True, eq=False, order=False, repr=False)
class Annotation(AnnotationBase):
"""Concrete base class for annotation instances.

Generated types that represent (subtypes of) `uima.tcas.Annotation` will
Expand Down Expand Up @@ -590,31 +613,40 @@ def __attrs_post_init__(self):
"""Build the constructor that can create feature structures of this type"""
name = _string_to_valid_classname(self.name)

# Determine whether this type is (transitively) a subtype of uima.tcas.Annotation
def _is_annotation_type(t: "Type") -> bool:
# Determine whether this type is (transitively) a subtype of a given type name
def _is_subtype_of(t: "Type", type_name: str) -> bool:
cur = t
while cur is not None:
if cur.name == TYPE_NAME_ANNOTATION:
if cur.name == type_name:
return True
cur = cur.supertype
return False

is_annotation_type = _is_annotation_type(self)
# Select the static base class and the set of features already provided by
# that base (so they are not redeclared as dynamic fields).
if _is_subtype_of(self, TYPE_NAME_ANNOTATION):
base = Annotation
inherited_features = {"sofa", "begin", "end"}
elif _is_subtype_of(self, TYPE_NAME_ANNOTATION_BASE):
base = AnnotationBase
inherited_features = {"sofa"}
else:
base = FeatureStructure
inherited_features = set()

# When inheriting from our concrete Annotation base, do not redeclare
# the 'begin' and 'end' features as fields; they are already present.
# Features provided by the static base class are not redeclared as fields.
fields = {}
for feature in self.all_features:
if feature.name in {"begin", "end"} and is_annotation_type:
# skip - Annotation base provides these
if feature.name in inherited_features:
# skip - the static base class provides this feature
continue
fields[feature.name] = attr.ib(default=None, repr=(feature.name != "sofa"))
fields["type"] = attr.ib(default=self)

# We assign this to a lambda to make it lazy
# When creating large type systems, almost no types are used so
# creating them on the fly is on average better
bases = (Annotation,) if is_annotation_type else (FeatureStructure,)
bases = (base,)

def _make_fs_class():
cls = attr.make_class(name, fields, bases=bases, slots=True, eq=False, order=False)
Expand Down
6 changes: 4 additions & 2 deletions src/cassis/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from collections import defaultdict
from functools import cmp_to_key
from io import IOBase, StringIO
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, cast

from cassis import Cas
from cassis.cas import Sofa
from cassis.typesystem import (
FEATURE_BASE_NAME_BEGIN,
FEATURE_BASE_NAME_END,
Expand All @@ -24,6 +25,7 @@
TYPE_NAME_LONG_ARRAY,
TYPE_NAME_SHORT_ARRAY,
TYPE_NAME_STRING_ARRAY,
AnnotationBase,
FeatureStructure,
Type,
is_annotation,
Expand Down Expand Up @@ -362,7 +364,7 @@ def _generate_anchor(
anchor += "*"

if mark_view and hasattr(fs, FEATURE_BASE_NAME_SOFA):
anchor += f"@{fs.sofa.sofaID}"
anchor += f"@{cast(Sofa, cast(AnnotationBase, fs).sofa).sofaID}"
Comment thread
reckart marked this conversation as resolved.

return anchor

Expand Down
4 changes: 2 additions & 2 deletions tests/test_cas.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ def test_sofa_mime_can_be_set_and_read():
def test_sofa_uri_can_be_set_and_read():
cas = Cas()

cas.sofa_uri = "https://raw.githubusercontent.com/dkpro/dkpro-cassis/main/README.md"
cas.sofa_uri = "https://raw.githubusercontent.com/dkpro/dkpro-cassis/master/README.rst"

assert cas.sofa_uri == "https://raw.githubusercontent.com/dkpro/dkpro-cassis/main/README.md"
assert cas.sofa_uri == "https://raw.githubusercontent.com/dkpro/dkpro-cassis/master/README.rst"


def test_sofa_string_can_be_set_using_constructor():
Expand Down