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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ hs_err_pid* # virtual machine crash logs, see http://www.java.com/en/download/he
**/__pycache__
**/dist
**/*.egg-info
**/.pytype
**/.mypy_cache
build/**
.coverage
coverage.xml
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ This means adding a properly structured module within the CLI package is all tha
pip install -e ".[dev]"
```

An editable install (`pip install -e .`) is required before running tests or building docs. The package uses `importlib.metadata` for version resolution at runtime, which only works when the package is registered in the environment. You can no longer run `pytest` or `pytype` directly against the source tree without installing first. If you want to avoid pulling in all dependencies, `pip install -e . --no-deps` is sufficient to register the package metadata.
An editable install (`pip install -e .`) is required before running tests or building docs. The package uses `importlib.metadata` for version resolution at runtime, which only works when the package is registered in the environment. You can no longer run `pytest` or `mypy` directly against the source tree without installing first. If you want to avoid pulling in all dependencies, `pip install -e . --no-deps` is sufficient to register the package metadata.

## Local Development

Expand Down
2 changes: 1 addition & 1 deletion build-tools/clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# Directories to remove
CLEAN_DIRS = [
"build", "dist", "*.egg-info", "mmif_python-*",
".pytest_cache", ".pytype", ".hypothesis",
".pytest_cache", ".mypy_cache", ".hypothesis",
"tests/.hypothesis", "htmlcov",
"docs-test",
]
Expand Down
8 changes: 4 additions & 4 deletions build-tools/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
This script is equivalent to::

pip install -e ".[test]"
pytype mmif
python -m mypy
python -m pytest --cov=mmif --cov-report=xml -m "not slow"
"""
import argparse
Expand Down Expand Up @@ -51,10 +51,10 @@ def main():
cwd=project_root,
)

# Run pytype static analysis
print("\n--- Running pytype ---")
# Run mypy static analysis (configured in pyproject.toml)
print("\n--- Running mypy ---")
run_command(
["pytype", "mmif"],
[sys.executable, "-m", "mypy"],
cwd=project_root,
)

Expand Down
1 change: 1 addition & 0 deletions documentation/target-versions.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"``mmif-python`` version","Target MMIF Specification"
`1.7.0 <https://pypi.org/project/mmif-python/1.7.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
`1.6.0 <https://pypi.org/project/mmif-python/1.6.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
`1.5.4 <https://pypi.org/project/mmif-python/1.5.4/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
`1.5.3 <https://pypi.org/project/mmif-python/1.5.3/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
Expand Down
11 changes: 7 additions & 4 deletions mmif/serialize/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
"""

from .annotation import *
from .annotation import __all__ as anno_all
from .mmif import *
from .mmif import __all__ as mmif_all
from .producer import *
from .view import *
from .view import __all__ as view_all

__all__ = anno_all + mmif_all + view_all
# a literal, so that type checkers can follow the star import of this package
__all__ = [
'Annotation', 'AnnotationProperties', 'Contain', 'Document',
'DocumentProperties', 'EmptyOutputWarning', 'Mmif', 'ProducerContext',
'Text', 'View', 'ViewMetadata', 'canonicalize_prop_name',
]
316 changes: 198 additions & 118 deletions mmif/serialize/annotation.py

Large diffs are not rendered by default.

534 changes: 366 additions & 168 deletions mmif/serialize/mmif.py

Large diffs are not rendered by default.

93 changes: 45 additions & 48 deletions mmif/serialize/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@

import json
import warnings
from datetime import datetime
from typing import Union, Any, Dict, Optional, TypeVar, Generic, Generator, Iterator, Type, Set, ClassVar, List
from collections.abc import Generator, Iterator
from typing import Any, ClassVar, Generic, TypeAlias, TypeVar, Union

T = TypeVar('T')
S = TypeVar('S')
PRMTV_TYPES: Type = Union[str, int, float, bool, None]
PRMTV_TYPES: TypeAlias = Union[str, int, float, bool, None] # noqa: UP007

__all__ = [
'MmifObject',
'MmifObjectEncoder',
'DataList',
'PRMTV_TYPES',
'DataDict',
'PRMTV_TYPES'
'DataList',
'MmifObject',
'MmifObjectEncoder'
]


Expand Down Expand Up @@ -96,20 +96,21 @@ class MmifObject(object):

# these are the reserved names that cannot be used as attribute names, and
# they won't be serialized
reserved_names: Set[str] = {
reserved_names: set[str] = {
'reserved_names',
'_unnamed_attributes',
'_attribute_classes',
'_required_attributes',
'_exclude_from_diff',
'_contextual_attributes'
}
_unnamed_attributes: Optional[dict]
_exclude_from_diff: Set[str]
_contextual_attributes: Set[str]
_attribute_classes: Dict[str, Type] = {} # Mapping: str -> Type
_unnamed_attributes: dict | None
_required_attributes: list[str]
_exclude_from_diff: set[str]
_contextual_attributes: set[str]
_attribute_classes: dict[str, type] = {} # Mapping: str -> Type

def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
def __init__(self, mmif_obj: bytes | str | dict | None = None, *_) -> None:
if isinstance(mmif_obj, bytes):
mmif_obj = mmif_obj.decode('utf8')
if not hasattr(self, '_required_attributes'):
Expand Down Expand Up @@ -159,7 +160,7 @@ def set_additional_property(self, key: str, value: Any) -> None:
stacklevel=2)
self[attr] = value
return
self._unnamed_attributes[key] = value # pytype: disable=unsupported-operands
self._unnamed_attributes[key] = value

def _named_attributes(self) -> Generator[str, None, None]:
"""
Expand All @@ -183,7 +184,7 @@ def serialize(self, pretty: bool = False, include_context: bool = True) -> str:
"""
return json.dumps(self._serialize(include_context=include_context), indent=2 if pretty else None, cls=MmifObjectEncoder)

def _serialize(self, alt_container: Optional[Dict] = None, include_context: bool = True) -> dict:
def _serialize(self, alt_container: dict | None = None, include_context: bool = True) -> dict:
"""
Maps a MMIF object to a plain python dict object,
rewriting internal keys that start with '_' to
Expand All @@ -198,8 +199,9 @@ def _serialize(self, alt_container: Optional[Dict] = None, include_context: bool
"""
container = alt_container if alt_container is not None else self._unnamed_attributes
serializing_obj = {}
try:
for k, v in container.items(): # pytype: disable=attribute-error
# `container` is None when additional properties are disallowed
if container is not None:
for k, v in container.items():
if v is None:
continue
k = str(k)
Expand All @@ -212,9 +214,6 @@ def _serialize(self, alt_container: Optional[Dict] = None, include_context: bool
serializing_obj[k] = v._serialize(include_context=include_context)
else:
serializing_obj[k] = v
except AttributeError as e:
# means _unnamed_attributes is None, so nothing unnamed would be serialized
pass
for k, v in self.__dict__.items():
if k in self.reserved_names:
continue
Expand Down Expand Up @@ -245,7 +244,7 @@ def is_empty(obj) -> bool:
return False

@staticmethod
def _load_json(json_obj: Union[dict, str]) -> dict:
def _load_json(json_obj: dict | str) -> dict:
"""
Maps JSON-format MMIF strings and dicts into Python dicts
with identifier-compliant keys. To do this, it replaces "@"
Expand All @@ -259,7 +258,7 @@ def _load_json(json_obj: Union[dict, str]) -> dict:
:param json_str: the JSON data to load and process
:return: the mapped data as a dict
"""
def from_atsign(d: Dict[str, Any]) -> dict:
def from_atsign(d: dict[str, Any]) -> dict:
for k in list(d.keys()):
if k.startswith('@'):
d[f'_{k[1:]}'] = d.pop(k)
Expand All @@ -280,7 +279,7 @@ def deep_from_atsign(d: dict) -> dict:
else:
raise TypeError(f"tried to load MMIF JSON in a format other than str or dict: {type(json_obj)}")

def deserialize(self, mmif_json: Union[str, dict]) -> None:
def deserialize(self, mmif_json: str | dict) -> None:
"""
Takes a JSON-formatted string or a simple `dict` that's json-loaded from
such a string as an input and populates object's fields with the values
Expand All @@ -298,9 +297,9 @@ def _deserialize(self, input_dict: dict) -> None:
If a subclass needs a special treatment during the mapping, it needs to
override this method.

This default method won't work for generic types (e.g. List[X], Dict[X, Y]).
This default method won't work for generic types (e.g. list[X], dict[X, Y]).
For now, lists are abstracted as DataList and dicts are abstracted as XXXMetadata classes.
However, if an attribute uses a generic type (e.g. view_metadata.contains: Dict[str, Contain])
However, if an attribute uses a generic type (e.g. view_metadata.contains: dict[str, Contain])
that class should override _deserialize of its own.

:param input_dict: the prepared JSON data that defines the object
Expand Down Expand Up @@ -351,7 +350,7 @@ def __setitem__(self, key, value) -> None:
else:
self.set_additional_property(key, value)

def __contains__(self, key: str) -> bool:
def __contains__(self, key: object) -> bool:
try:
self.__getitem__(key)
return True
Expand Down Expand Up @@ -450,42 +449,42 @@ class DataList(MmifObject, Generic[T]):

:param Union[str, list] mmif_obj: the data that the list contains
"""
def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None, *_):
def __init__(self, mmif_obj: bytes | str | list | None = None, *_):
self.reserved_names.add('_items')
self._items: Dict[str, T] = dict()
self._items: dict[str, T] = dict()
self.disallow_additional_properties()
if mmif_obj is None:
mmif_obj = []
super().__init__(mmif_obj)
super().__init__(mmif_obj) # type: ignore[arg-type]

def _serialize(self, *args, **kwargs) -> list: # pytype: disable=signature-mismatch
def _serialize(self, *args, **kwargs) -> list: # type: ignore[override]
"""
Internal serialization method. Returns a list.

:return: list of the values of the internal dictionary.
"""
return list(super()._serialize(self._items, **kwargs).values())

def deserialize(self, mmif_json: Union[str, list]) -> None: # pytype: disable=signature-mismatch
def deserialize(self, mmif_json: str | list) -> None: # type: ignore[override]
"""
Passes the input data into the internal deserializer.
"""
super().deserialize(mmif_json)
super().deserialize(mmif_json) # type: ignore[arg-type]

@staticmethod
def _load_json(json_list: Union[list, str]) -> list:
def _load_json(json_list: list | str) -> list: # type: ignore[override]
if type(json_list) is str:
json_list = json.loads(json_list)
return [MmifObject._load_json(obj) for obj in json_list]

def _deserialize(self, input_list: list) -> None:
def _deserialize(self, input_list: list) -> None: # type: ignore[override]
raise NotImplementedError()

def get(self, key: str, default=None) -> Optional[T]:
def get(self, key: str, default=None) -> T | None:
"""
.. deprecated:: 1.1.3
Do not use in new code. Will be removed in 2.0.0.
Use container-level access or positional indexing instead.
Will be removed in 2.0.0. Use container-level access or positional
indexing instead.

Deprecated method for retrieving list elements by string ID.

Expand Down Expand Up @@ -517,13 +516,11 @@ def get(self, key: str, default=None) -> Optional[T]:
--------
__getitem__ : List-style positional access with integers
"""
warnings.warn(
"The 'get' method on list-like collections is deprecated and "
"will be removed in 2.0.0. Use container-level access "
"(e.g., mmif['v1']) or positional indexing (e.g., views[0]).",
DeprecationWarning,
stacklevel=2
)
warnings.warn("get() on list-like collections is deprecated and will "
"be removed in 2.0.0. Use container-level access (e.g.,"
" mmif['v1']) or positional indexing (e.g., views[0]) "
"instead.",
DeprecationWarning, stacklevel=2)
return self._items.get(key, default)

def _append_with_key(self, key: str, value: T, overwrite=False) -> None:
Expand All @@ -549,7 +546,7 @@ def _append_with_key(self, key: str, value: T, overwrite=False) -> None:
def append(self, value, overwrite):
raise NotImplementedError()

def __getitem__(self, key: Union[int, slice]) -> Union[T, List[T]]:
def __getitem__(self, key: int | slice) -> T | list[T]:
"""
List-style positional access using integers or slices.

Expand Down Expand Up @@ -613,9 +610,9 @@ def empty(self):


class DataDict(MmifObject, Generic[T, S]):
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_):
def __init__(self, mmif_obj: bytes | str | dict | None = None, *_):
self.reserved_names.add('_items')
self._items: Dict[T, S] = dict()
self._items: dict[T, S] = dict()
self.disallow_additional_properties()
if mmif_obj is None:
mmif_obj = {}
Expand All @@ -624,7 +621,7 @@ def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_):
def _serialize(self, *args, **kwargs) -> dict:
return super()._serialize(self._items, **kwargs)

def get(self, key: T, default=None) -> Optional[S]:
def get(self, key: T, default=None) -> S | None:
"""
Dictionary-style safe access with optional default value.

Expand Down
Loading
Loading