Source code for decomp.semantics.uds.metadata

"""Metadata structures for Universal Decompositional Semantics (UDS) annotations.

This module defines the metadata infrastructure used to describe and validate
UDS semantic annotations across sentence and document graphs. It provides a
flexible type system that supports both categorical and continuous values
with optional bounds and ordering constraints.

Key Components
--------------
Type System
    - :data:`PrimitiveType`: Base types supported in UDS (str, int, bool, float)
    - :data:`UDSDataTypeDict`: Dictionary format for serializing data types
    - :class:`UDSDataType`: Wrapper for primitive types with categorical support

Property Metadata
    - :data:`PropertyMetadataDict`: Dictionary format for property metadata
    - :class:`UDSPropertyMetadata`: Metadata for individual semantic properties

Annotation Metadata
    - :data:`AnnotationMetadataDict`: Dictionary format for annotation metadata
    - :class:`UDSAnnotationMetadata`: Collection of properties organized by subspace
    - :class:`UDSCorpusMetadata`: Complete metadata for sentence and document graphs

The metadata system ensures consistency across UDS corpora by tracking:
- Property names and their expected data types
- Categorical values and their ordering
- Numeric bounds for continuous properties
- Confidence score types for uncertain annotations
- Subspace organization of semantic properties

See Also
--------
decomp.semantics.uds.annotation : Annotation classes that use this metadata
decomp.semantics.uds.corpus : Corpus classes that store metadata
"""

from collections import defaultdict
from typing import Literal, cast

from decomp.semantics.uds.types import UDSSubspace


# Type aliases for UDS metadata structures
type PrimitiveType = str | int | bool | float
"""Union of primitive types supported in UDS annotations: str, int, bool, float."""

type UDSDataTypeDict = dict[
    str,
    str | list[PrimitiveType] | bool | float
]
"""Dictionary representation of a UDS data type with optional categories and bounds."""

type PropertyMetadataDict = dict[
    str,
    list[str] | dict[str, UDSDataTypeDict]
]
"""Dictionary representation of property metadata including value/confidence types.

Note: While annotators are stored internally as sets, they are serialized as lists
for JSON compatibility.
"""

type AnnotationMetadataDict = dict[
    str,
    dict[str, PropertyMetadataDict]
]
"""Dictionary mapping subspaces to their property metadata."""


def _dtype(name: str) -> type[PrimitiveType]:
    """Convert string representation to a primitive type class.

    Only ``str``, ``int``, ``bool``, and ``float`` are supported.

    Parameters
    ----------
    name : str
        A string representing the type ("str", "int", "bool", or "float").

    Returns
    -------
    type[PrimitiveType]
        The corresponding type class.

    Raises
    ------
    ValueError
        If name is not one of the supported type strings.
    """
    if name == 'str':
        return str
    elif name == 'int':
        return int
    elif name == 'bool':
        return bool
    elif name == 'float':
        return float
    else:
        raise ValueError(
            f'name must be "str", "int", "bool", or "float", not {name}'
        )


[docs] class UDSDataType: """A wrapper around builtin datatypes with support for categorical values. This class provides a minimal extension of basic builtin datatypes for representing categorical datatypes with optional ordering and bounds. It serves as a lightweight alternative to `pandas` categorical types. Parameters ---------- datatype : type[PrimitiveType] A builtin datatype (str, int, bool, or float). categories : list[PrimitiveType] | None, optional The allowed values for categorical datatypes. Required if ordered is True. ordered : bool | None, optional Whether this categorical datatype has an ordering. Required if categories is specified. lower_bound : float | None, optional The lower bound value for numeric types. Can be specified independently of categories. If both categories and lower_bound are specified, the datatype must be ordered and bounds must match category bounds. upper_bound : float | None, optional The upper bound value for numeric types. Can be specified independently of categories. If both categories and upper_bound are specified, the datatype must be ordered and bounds must match category bounds. Attributes ---------- datatype : type[PrimitiveType] The underlying primitive type. is_categorical : bool Whether this represents a categorical datatype. is_ordered_categorical : bool Whether this is an ordered categorical datatype. is_ordered_noncategorical : bool Whether this is ordered but not categorical (has bounds). lower_bound : float | None The lower bound if specified. upper_bound : float | None The upper bound if specified. categories : set[PrimitiveType] | list[PrimitiveType] | None The categories as a set (unordered) or list (ordered). """
[docs] def __init__( self, datatype: type[PrimitiveType], categories: list[PrimitiveType] | None = None, ordered: bool | None = None, lower_bound: float | None = None, upper_bound: float | None = None ) -> None: self._validate( datatype, categories, ordered, lower_bound, upper_bound ) self._datatype: type[PrimitiveType] = datatype self._categories: list[PrimitiveType] | set[PrimitiveType] | None = ( categories ) self._ordered: bool | None = ordered self._lower_bound: float | None = lower_bound self._upper_bound: float | None = upper_bound if ordered and categories is not None: if lower_bound is None: # for ordered categories, bounds should be numeric first_cat = categories[0] if isinstance(first_cat, int | float): self._lower_bound = float(first_cat) if upper_bound is None: # for ordered categories, bounds should be numeric last_cat = categories[-1] if isinstance(last_cat, int | float): self._upper_bound = float(last_cat) elif categories is not None: self._categories = set(categories) elif lower_bound is not None or upper_bound is not None: self._ordered = True
def _validate( self, datatype: type[PrimitiveType], categories: list[PrimitiveType] | None, ordered: bool | None, lower_bound: float | None, upper_bound: float | None ) -> None: """Validate datatype parameters for consistency. Parameters ---------- datatype : type[PrimitiveType] The primitive type. categories : list[PrimitiveType] | None Optional category values. ordered : bool | None Whether categories are ordered. lower_bound : float | None Optional lower bound. upper_bound : float | None Optional upper bound. Raises ------ ValueError If the parameter combination is invalid. """ if ( ordered is not None and categories is None and lower_bound is None and upper_bound is None ): raise ValueError( 'if ordered is specified either categories or ' 'lower_bound and/or upper_bound must be also' ) if categories is not None and ordered is None: raise ValueError( 'if categories is specified ordered must be specified also' ) if categories is not None and datatype not in [str, int]: raise ValueError( 'categorical variable must be str- or int-valued' ) if lower_bound is not None or upper_bound is not None: if categories is not None and not ordered: raise ValueError( 'if categorical datatype is unordered, upper ' 'and lower bounds should not be specified' ) if ( categories is not None and lower_bound is not None and lower_bound != categories[0] ): raise ValueError( 'lower bound does not match categories lower bound' ) if ( categories is not None and upper_bound is not None and upper_bound != categories[-1] ): raise ValueError( 'upper bound does not match categories upper bound' )
[docs] def __eq__(self, other: object) -> bool: """Check equality based on dictionary representation. Parameters ---------- other : object Object to compare with. Returns ------- bool True if both objects have the same dictionary representation. """ if not isinstance(other, UDSDataType): return NotImplemented self_dict = self.to_dict() other_dict = other.to_dict() return all(other_dict[k] == v for k, v in self_dict.items())
@property def datatype(self) -> type[PrimitiveType]: """The underlying primitive type. Returns ------- type[PrimitiveType] The primitive type (str, int, bool, or float). """ return self._datatype @property def is_categorical(self) -> bool: """Whether this datatype has defined categories. Returns ------- bool True if categories are defined. """ return self._categories is not None @property def is_ordered_categorical(self) -> bool: """Whether this is a categorical datatype with ordering. Returns ------- bool True if categorical and ordered. """ return self.is_categorical and bool(self._ordered) @property def is_ordered_noncategorical(self) -> bool: """Whether this has ordering but no categories (bounded numeric). Returns ------- bool True if ordered but not categorical. """ return not self.is_categorical and bool(self._ordered) @property def lower_bound(self) -> float | None: """The lower bound value if specified. Returns ------- float | None The lower bound or None. """ return self._lower_bound @property def upper_bound(self) -> float | None: """The upper bound value if specified. Returns ------- float | None The upper bound or None. """ return self._upper_bound @property def categories(self) -> set[PrimitiveType] | list[PrimitiveType] | None: """The allowed category values. Returns a set if the datatype is unordered categorical and a list if it is ordered categorical. Returns ------- set[PrimitiveType] | list[PrimitiveType] | None Categories as set (unordered), list (ordered), or None. Raises ------ AttributeError If this is not a categorical datatype. """ if self._categories is None: raise AttributeError('not a categorical dtype') return self._categories
[docs] @classmethod def from_dict(cls, datatype: UDSDataTypeDict) -> 'UDSDataType': """Build a UDSDataType from a dictionary. Parameters ---------- datatype A dictionary representing a datatype. This dictionary must at least have a ``"datatype"`` key. It may also have a ``"categorical"`` and an ``"ordered"`` key, in which case it must have both. """ if any( k not in [ 'datatype', 'categories', 'ordered', 'lower_bound', 'upper_bound' ] for k in datatype ): raise KeyError( f'dictionary defining datatype has keys ' f'{", ".join(f'"{k}"' for k in datatype)} ' f'but it may only have "datatype", "categories", ' f'"ordered", "lower_bound", and "upper_bound" as keys' ) if 'datatype' in datatype: datatype_value = datatype['datatype'] if not isinstance(datatype_value, str): raise TypeError('datatype must be a string') typ = _dtype(datatype_value) else: raise KeyError('must specify "datatype" field') if 'categories' in datatype and datatype['categories'] is not None: categories_value = datatype['categories'] if not isinstance(categories_value, list): raise TypeError('categories must be a list') cats = [typ(c) for c in categories_value] else: cats = None ordered_value = datatype.get('ordered') ordered = bool(ordered_value) if ordered_value is not None else None lower_bound_value = datatype.get('lower_bound') if ( lower_bound_value is not None and isinstance(lower_bound_value, int | float | str) ): lower_bound = float(lower_bound_value) else: lower_bound = None upper_bound_value = datatype.get('upper_bound') if ( upper_bound_value is not None and isinstance(upper_bound_value, int | float | str) ): upper_bound = float(upper_bound_value) else: upper_bound = None return cls(typ, cats, ordered, lower_bound, upper_bound)
[docs] def to_dict(self) -> UDSDataTypeDict: """Convert to dictionary representation. Returns ------- UDSDataTypeDict Dictionary with datatype info, excluding None values. """ with_null: dict[str, str | list[PrimitiveType] | bool | float | None] = { 'datatype': self._datatype.__name__, 'categories': ( list(self._categories) if isinstance(self._categories, set) else self._categories ), 'ordered': self._ordered, 'lower_bound': self._lower_bound, 'upper_bound': self._upper_bound } # filter out None values and ensure types match UDSDataTypeDict result: UDSDataTypeDict = {} for k, v in with_null.items(): if v is not None: result[k] = v return result
[docs] class UDSPropertyMetadata: """Metadata for a UDS property including value and confidence datatypes. This class encapsulates the metadata for a single UDS property, including the datatypes for both the property value and the confidence score, as well as optional annotator information. Parameters ---------- value : UDSDataType The datatype for property values. confidence : UDSDataType The datatype for confidence scores. annotators : set[str] | None, optional Set of annotator identifiers who provided annotations for this property. Attributes ---------- value : UDSDataType The value datatype. confidence : UDSDataType The confidence datatype. annotators : set[str] | None The annotator identifiers. """
[docs] def __init__( self, value: UDSDataType, confidence: UDSDataType, annotators: set[str] | None = None ) -> None: self._value = value self._confidence = confidence self._annotators = annotators
[docs] def __eq__(self, other: object) -> bool: """Whether the value and confidence datatypes match and annotators are equal. Parameters ---------- other the other UDSDatatype. """ if not isinstance(other, UDSPropertyMetadata): return NotImplemented return ( self.value == other.value and self.confidence == other.confidence and self.annotators == other.annotators )
[docs] def __add__(self, other: 'UDSPropertyMetadata') -> 'UDSPropertyMetadata': """Return a UDSPropertyMetadata with the union of annotators. If the value and confidence datatypes don't match, this raises an error. Parameters ---------- other the other UDSDatatype. Raises ------ ValueError Raised if the value and confidence datatypes don't match. """ if self.value != other.value or self.confidence != other.confidence: raise ValueError( 'Cannot add metadata whose value and confidence ' 'datatypes are not equal' ) if self.annotators is None and other.annotators is None: return self elif self.annotators is None: return UDSPropertyMetadata( self.value, self.confidence, other.annotators ) elif other.annotators is None: return UDSPropertyMetadata( self.value, self.confidence, self.annotators ) else: return UDSPropertyMetadata( self.value, self.confidence, self.annotators | other.annotators )
@property def value(self) -> UDSDataType: """The datatype for property values. Returns ------- UDSDataType The value datatype. """ return self._value @property def confidence(self) -> UDSDataType: """The datatype for confidence scores. Returns ------- UDSDataType The confidence datatype. """ return self._confidence @property def annotators(self) -> set[str] | None: """The set of annotator identifiers. Returns ------- set[str] | None Annotator IDs or None if not tracked. """ return self._annotators
[docs] @classmethod def from_dict( cls, metadata: PropertyMetadataDict ) -> 'UDSPropertyMetadata': """Build UDSPropertyMetadata from a dictionary. Parameters ---------- metadata : PropertyMetadataDict A mapping from ``"value"`` and ``"confidence"`` to datatype dictionaries. May optionally include ``"annotators"`` mapping to a set of annotator identifiers. Returns ------- UDSPropertyMetadata The constructed metadata object. Raises ------ ValueError If required fields (value, confidence) are missing. TypeError If fields have incorrect types """ required = {'value', 'confidence'} missing = required - set(metadata) if missing: raise ValueError( f'the following metadata fields are missing: {", ".join(missing)}' ) value_data_raw = metadata['value'] confidence_data_raw = metadata['confidence'] if not isinstance(value_data_raw, dict): raise TypeError('value must be a dictionary') if not isinstance(confidence_data_raw, dict): raise TypeError('confidence must be a dictionary') # these should be UDSDataTypeDict, not nested dicts value_data = cast(UDSDataTypeDict, value_data_raw) confidence_data = cast(UDSDataTypeDict, confidence_data_raw) value = UDSDataType.from_dict(value_data) confidence = UDSDataType.from_dict(confidence_data) if 'annotators' not in metadata or metadata['annotators'] is None: return UDSPropertyMetadata(value, confidence) else: annotators = set(metadata['annotators']) return UDSPropertyMetadata(value, confidence, annotators)
[docs] def to_dict(self) -> PropertyMetadataDict: """Convert to dictionary representation. Returns ------- PropertyMetadataDict Dictionary with value, confidence, and optional annotators. """ datatypes: dict[str, UDSDataTypeDict] = { 'value': self._value.to_dict(), 'confidence': self._confidence.to_dict() } if self._annotators is not None: # return type needs to match PropertyMetadataDict # Convert set to list for JSON serialization result: PropertyMetadataDict = {'annotators': list(self._annotators)} # cast datatypes to the appropriate type for PropertyMetadataDict result.update( cast(PropertyMetadataDict, datatypes) ) return result else: return cast(PropertyMetadataDict, datatypes)
[docs] class UDSAnnotationMetadata: """The metadata for UDS properties by subspace. Parameters ---------- metadata A mapping from subspaces to properties to datatypes and possibly annotators. """
[docs] def __init__( self, metadata: dict[UDSSubspace, dict[str, UDSPropertyMetadata]] ): self._metadata = metadata
[docs] def __getitem__( self, k: UDSSubspace | tuple[UDSSubspace, str] ) -> dict[str, UDSPropertyMetadata] | UDSPropertyMetadata: """Get metadata by subspace or (subspace, property) tuple. Parameters ---------- k : UDSSubspace | tuple[UDSSubspace, str] Either a subspace name or a (subspace, property) tuple. Returns ------- dict[str, UDSPropertyMetadata] | UDSPropertyMetadata Property dict for subspace or specific property metadata. Raises ------ TypeError If key is not a string or 2-tuple. KeyError If subspace or property not found. """ if isinstance(k, str): return self._metadata[k] elif isinstance(k, tuple) and len(k) == 2: # for tuple access like metadata[subspace, property] subspace, prop = k return self._metadata[subspace][prop] else: raise TypeError("Key must be a string or 2-tuple")
[docs] def __eq__(self, other: object) -> bool: """Check equality by comparing all subspaces and properties. Parameters ---------- other : object Object to compare with. Returns ------- bool True if all subspaces, properties, and metadata match. """ if not isinstance(other, UDSAnnotationMetadata): return NotImplemented if self.subspaces != other.subspaces: return False for ss in self.subspaces: if self.properties(ss) != other.properties(ss): return False for prop in self.properties(ss): if self[ss, prop] != other[ss, prop]: return False return True
[docs] def __add__( self, other: 'UDSAnnotationMetadata' ) -> 'UDSAnnotationMetadata': """Merge two metadata objects, combining annotators for shared properties. Parameters ---------- other : UDSAnnotationMetadata Metadata to merge with this one. Returns ------- UDSAnnotationMetadata New metadata with merged properties and annotators. """ new_metadata = defaultdict(dict, self.metadata) for subspace, propdict in other.metadata.items(): for prop, md in propdict.items(): if prop in new_metadata[subspace]: new_metadata[subspace][prop] += md else: new_metadata[subspace][prop] = md return UDSAnnotationMetadata(new_metadata)
@property def metadata(self) -> dict[UDSSubspace, dict[str, UDSPropertyMetadata]]: """The underlying metadata dictionary. Returns ------- dict[UDSSubspace, dict[str, UDSPropertyMetadata]] Mapping from subspaces to properties to metadata. """ return self._metadata @property def subspaces(self) -> set[UDSSubspace]: """Set of all subspace names. Returns ------- set[UDSSubspace] The subspace identifiers. """ return set(self._metadata.keys())
[docs] def properties(self, subspace: UDSSubspace | None = None) -> set[str]: """Return the properties in a subspace. Parameters ---------- subspace The subspace to get the properties of. """ if subspace is None: return {prop for propdict in self._metadata.values() for prop in propdict} else: return set(self._metadata[subspace])
def annotators( self, subspace: UDSSubspace | None = None, prop: str | None = None ) -> set[str] | None: """Get annotator IDs for a subspace and/or property. Parameters ---------- subspace : UDSSubspace | None, optional Subspace to filter by. If None, gets all annotators. prop : str | None, optional Property to filter by. Requires subspace if specified. Returns ------- set[str] | None Union of annotator IDs, or None if no annotators found. Raises ------ ValueError If prop is specified without subspace. """ if subspace is None and prop is not None: errmsg = 'subspace must be specified if prop is specified' raise ValueError(errmsg) if subspace is None: annotators: list[set[str]] = [ md.annotators for propdict in self._metadata.values() for md in propdict.values() if md.annotators is not None ] elif prop is None: annotators = [ md.annotators for md in self._metadata[subspace].values() if md.annotators is not None ] elif self._metadata[subspace][prop].annotators is None: annotators = [] else: ann_set = self._metadata[subspace][prop].annotators annotators = [ann_set] if ann_set is not None else [] if not annotators: return None else: return {ann for part in annotators for ann in part}
[docs] def has_annotators( self, subspace: UDSSubspace | None = None, prop: str | None = None ) -> bool: """Check if annotators exist for a subspace and/or property. Parameters ---------- subspace : UDSSubspace | None, optional Subspace to check. prop : str | None, optional Property to check. Returns ------- bool True if any annotators exist. """ return bool(self.annotators(subspace, prop))
[docs] @classmethod def from_dict( cls, metadata: AnnotationMetadataDict ) -> 'UDSAnnotationMetadata': """Build from nested dictionary structure. Parameters ---------- metadata : AnnotationMetadataDict Nested dict mapping subspaces to properties to metadata dicts. Returns ------- UDSAnnotationMetadata The constructed metadata object. """ return cls({ cast(UDSSubspace, subspace): { prop: UDSPropertyMetadata.from_dict(md) for prop, md in propdict.items() } for subspace, propdict in metadata.items() })
[docs] def to_dict(self) -> AnnotationMetadataDict: """Convert to nested dictionary structure. Returns ------- AnnotationMetadataDict Nested dict representation. """ return { subspace: { prop: md.to_dict() for prop, md in propdict.items() } for subspace, propdict in self._metadata.items() }
[docs] class UDSCorpusMetadata: """The metadata for UDS properties by subspace. This is a thin wrapper around a pair of ``UDSAnnotationMetadata`` objects: one for sentence annotations and one for document annotations. Parameters ---------- sentence_metadata The metadata for sentence annotations. document_metadata The metadata for document_annotations. """
[docs] def __init__( self, sentence_metadata: UDSAnnotationMetadata | None = None, document_metadata: UDSAnnotationMetadata | None = None ) -> None: self._sentence_metadata = ( sentence_metadata if sentence_metadata is not None else UDSAnnotationMetadata({}) ) self._document_metadata = ( document_metadata if document_metadata is not None else UDSAnnotationMetadata({}) )
[docs] @classmethod def from_dict( cls, metadata: dict[ Literal['sentence_metadata', 'document_metadata'], AnnotationMetadataDict ] ) -> 'UDSCorpusMetadata': """Build from dictionary with sentence and document metadata. Parameters ---------- metadata : dict[ Literal['sentence_metadata', 'document_metadata'], AnnotationMetadataDict ] Dict with 'sentence_metadata' and 'document_metadata' keys. Returns ------- UDSCorpusMetadata The constructed corpus metadata. """ return cls( UDSAnnotationMetadata.from_dict( metadata['sentence_metadata'] ), UDSAnnotationMetadata.from_dict( metadata['document_metadata'] ) )
[docs] def to_dict(self) -> dict[ Literal['sentence_metadata', 'document_metadata'], AnnotationMetadataDict ]: """Convert to dictionary with sentence and document metadata. Returns ------- dict[Literal['sentence_metadata', 'document_metadata'], AnnotationMetadataDict] Dict with 'sentence_metadata' and 'document_metadata' keys. """ return { 'sentence_metadata': self._sentence_metadata.to_dict(), 'document_metadata': self._document_metadata.to_dict() }
[docs] def __add__(self, other: 'UDSCorpusMetadata') -> 'UDSCorpusMetadata': """Merge two corpus metadata objects. Parameters ---------- other : UDSCorpusMetadata Metadata to merge. Returns ------- UDSCorpusMetadata New metadata with merged sentence and document metadata. """ new_sentence_metadata = self._sentence_metadata + other._sentence_metadata new_document_metadata = self._document_metadata + other._document_metadata return self.__class__(new_sentence_metadata, new_document_metadata)
[docs] def add_sentence_metadata(self, metadata: UDSAnnotationMetadata) -> None: """Add sentence annotation metadata. Parameters ---------- metadata : UDSAnnotationMetadata Metadata to merge with existing sentence metadata. """ self._sentence_metadata += metadata
[docs] def add_document_metadata(self, metadata: UDSAnnotationMetadata) -> None: """Add document annotation metadata. Parameters ---------- metadata : UDSAnnotationMetadata Metadata to merge with existing document metadata. """ self._document_metadata += metadata
@property def sentence_metadata(self) -> UDSAnnotationMetadata: """The sentence-level annotation metadata. Returns ------- UDSAnnotationMetadata Metadata for sentence annotations. """ return self._sentence_metadata @property def document_metadata(self) -> UDSAnnotationMetadata: """The document-level annotation metadata. Returns ------- UDSAnnotationMetadata Metadata for document annotations. """ return self._document_metadata @property def sentence_subspaces(self) -> set[UDSSubspace]: """Set of sentence-level subspaces. Returns ------- set[UDSSubspace] Sentence subspace identifiers. """ return self._sentence_metadata.subspaces @property def document_subspaces(self) -> set[UDSSubspace]: """Set of document-level subspaces. Returns ------- set[UDSSubspace] Document subspace identifiers. """ return self._document_metadata.subspaces
[docs] def sentence_properties(self, subspace: UDSSubspace | None = None) -> set[str]: """Return the properties in a sentence subspace. Parameters ---------- subspace The subspace to get the properties of. """ return self._sentence_metadata.properties(subspace)
[docs] def document_properties(self, subspace: UDSSubspace | None = None) -> set[str]: """Return the properties in a document subspace. Parameters ---------- subspace The subspace to get the properties of. """ return self._document_metadata.properties(subspace)
[docs] def sentence_annotators( self, subspace: UDSSubspace | None = None, prop: str | None = None ) -> set[str] | None: """Return the annotators for a property in a sentence subspace. Parameters ---------- subspace The subspace to get the annotators of. prop The property to get the annotators of. """ return self._sentence_metadata.annotators(subspace, prop)
[docs] def document_annotators( self, subspace: UDSSubspace | None = None, prop: str | None = None ) -> set[str] | None: """Return the annotators for a property in a document subspace. Parameters ---------- subspace The subspace to get the annotators of. prop The property to get the annotators of. """ return self._document_metadata.annotators(subspace, prop)
[docs] def has_sentence_annotators( self, subspace: UDSSubspace | None = None, prop: str | None = None ) -> bool: """Check if sentence-level annotators exist. Parameters ---------- subspace : UDSSubspace | None, optional Subspace to check. prop : str | None, optional Property to check. Returns ------- bool True if annotators exist. """ return self._sentence_metadata.has_annotators(subspace, prop)
[docs] def has_document_annotators( self, subspace: UDSSubspace | None = None, prop: str | None = None ) -> bool: """Check if document-level annotators exist. Parameters ---------- subspace : UDSSubspace | None, optional Subspace to check. prop : str | None, optional Property to check. Returns ------- bool True if annotators exist. """ return self._document_metadata.has_annotators(subspace, prop)