mmif-python 1.5.3__tar.gz → 1.5.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. {mmif_python-1.5.3 → mmif_python-1.5.4}/CHANGELOG.md +12 -0
  2. {mmif_python-1.5.3 → mmif_python-1.5.4}/PKG-INFO +2 -2
  3. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/target-versions.csv +1 -0
  4. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/serialize/annotation.py +125 -65
  5. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/serialize/mmif.py +3 -17
  6. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/serialize/view.py +17 -8
  7. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/summarizer/nodes.py +64 -25
  8. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/summarizer/summary.py +3 -3
  9. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/video_document_helper.py +217 -37
  10. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif_python.egg-info/PKG-INFO +2 -2
  11. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif_python.egg-info/requires.txt +1 -1
  12. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif_python.egg-info/scm_file_list.json +64 -64
  13. mmif_python-1.5.4/mmif_python.egg-info/scm_version.json +8 -0
  14. {mmif_python-1.5.3 → mmif_python-1.5.4}/pyproject.toml +1 -1
  15. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/test_serialize.py +135 -66
  16. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/test_utils.py +425 -57
  17. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/test_utils_cli.py +23 -0
  18. mmif_python-1.5.3/mmif_python.egg-info/scm_version.json +0 -8
  19. {mmif_python-1.5.3 → mmif_python-1.5.4}/.github/workflows/codecov.yml +0 -0
  20. {mmif_python-1.5.3 → mmif_python-1.5.4}/.github/workflows/issue-assign.yml +0 -0
  21. {mmif_python-1.5.3 → mmif_python-1.5.4}/.github/workflows/issue-close.yml +0 -0
  22. {mmif_python-1.5.3 → mmif_python-1.5.4}/.github/workflows/issue-infra-project.yml +0 -0
  23. {mmif_python-1.5.3 → mmif_python-1.5.4}/.github/workflows/publish.yml +0 -0
  24. {mmif_python-1.5.3 → mmif_python-1.5.4}/.github/workflows/release-check.yml +0 -0
  25. {mmif_python-1.5.3 → mmif_python-1.5.4}/.gitignore +0 -0
  26. {mmif_python-1.5.3 → mmif_python-1.5.4}/CONTRIBUTING.md +0 -0
  27. {mmif_python-1.5.3 → mmif_python-1.5.4}/LICENSE +0 -0
  28. {mmif_python-1.5.3 → mmif_python-1.5.4}/README.md +0 -0
  29. {mmif_python-1.5.3 → mmif_python-1.5.4}/build-tools/build.py +0 -0
  30. {mmif_python-1.5.3 → mmif_python-1.5.4}/build-tools/clean.py +0 -0
  31. {mmif_python-1.5.3 → mmif_python-1.5.4}/build-tools/docs.py +0 -0
  32. {mmif_python-1.5.3 → mmif_python-1.5.4}/build-tools/prep_release.py +0 -0
  33. {mmif_python-1.5.3 → mmif_python-1.5.4}/build-tools/publish.py +0 -0
  34. {mmif_python-1.5.3 → mmif_python-1.5.4}/build-tools/test.py +0 -0
  35. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/_mmif_example_builder/__init__.py +0 -0
  36. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/_templates/versions.html +0 -0
  37. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/cli.rst +0 -0
  38. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/conf.py +0 -0
  39. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/consumer-tutorial.rst +0 -0
  40. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/index.rst +0 -0
  41. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/introduction.rst +0 -0
  42. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/plugins.rst +0 -0
  43. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/summarizer.rst +0 -0
  44. {mmif_python-1.5.3 → mmif_python-1.5.4}/documentation/target-versions.rst +0 -0
  45. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/__init__.py +0 -0
  46. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/res/__init__.py +0 -0
  47. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/res/mmif.json +0 -0
  48. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/serialize/__init__.py +0 -0
  49. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/serialize/model.py +0 -0
  50. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/__init__.py +0 -0
  51. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/cli/__init__.py +0 -0
  52. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/cli/describe.py +0 -0
  53. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/cli/rewind.py +0 -0
  54. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/cli/source.py +0 -0
  55. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/cli/summarize.py +0 -0
  56. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/sequence_helper.py +0 -0
  57. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/summarizer/__init__.py +0 -0
  58. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/summarizer/config.py +0 -0
  59. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/summarizer/graph.py +0 -0
  60. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/summarizer/utils.py +0 -0
  61. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/text_document_helper.py +0 -0
  62. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/timeunit_helper.py +0 -0
  63. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/utils/workflow_helper.py +0 -0
  64. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/ver/__init__.py +0 -0
  65. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/vocabulary/__init__.py +0 -0
  66. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/vocabulary/annotation_types.py +0 -0
  67. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/vocabulary/base_types.py +0 -0
  68. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif/vocabulary/document_types.py +0 -0
  69. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif_docloc_http/__init__.py +0 -0
  70. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif_python.egg-info/SOURCES.txt +0 -0
  71. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif_python.egg-info/dependency_links.txt +0 -0
  72. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif_python.egg-info/entry_points.txt +0 -0
  73. {mmif_python-1.5.3 → mmif_python-1.5.4}/mmif_python.egg-info/top_level.txt +0 -0
  74. {mmif_python-1.5.3 → mmif_python-1.5.4}/setup.cfg +0 -0
  75. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/black-2997fps.mp4 +0 -0
  76. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/black-2997fps.webm +0 -0
  77. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
  78. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/README.md +0 -0
  79. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
  80. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
  81. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/everything/raw.json +0 -0
  82. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/examples.json +0 -0
  83. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/README.md +0 -0
  84. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
  85. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/image-box-ocr.json +0 -0
  86. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/image-box.json +0 -0
  87. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/image.json +0 -0
  88. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/slate-detect.json +0 -0
  89. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/slate-reco.json +0 -0
  90. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/video-demux.json +0 -0
  91. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
  92. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
  93. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
  94. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
  95. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript.json +0 -0
  96. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/others/video.json +0 -0
  97. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
  98. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif-examples/swt-1.0.mmif +0 -0
  99. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/mmif_examples.py +0 -0
  100. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/test_specver.py +0 -0
  101. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/test_vocab.py +0 -0
  102. {mmif_python-1.5.3 → mmif_python-1.5.4}/tests/testsrc-2997fps-ptsoffset.mp4 +0 -0
@@ -1,4 +1,16 @@
1
1
 
2
+ ## releasing 1.5.3 (2026-07-08)
3
+ ### Overview
4
+ This patch includes improvements in automatic conversion between contains-metadata properties and annotation properties (#396)
5
+
6
+ ### Additions
7
+ * automatic "factoring" of value-shared properties from annotation-level to contains-level at serialization time works much more generally now (formerly only worked for `Annotation` type and `document` prop)
8
+
9
+ ### Changes
10
+ * fixed a bug in reading old MMIF files with non-canonical prop names (contains-level prop value was not properly propagated at deserialization for those non-canonical named props)
11
+
12
+
13
+
2
14
  ## releasing 1.5.2 (2026-07-07)
3
15
  ### Overview
4
16
  This is a quick patch on the bug that when MMIF is deserialized, the annotation ID counts in the views weren't properly initialized. This should not be a real bug since new annotations should always written to new views (of current processing app) but not to existing views (where annotation count > 0), however, bugs in app code can trigger this condition, and this patch preventively fixes such cases.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmif-python
3
- Version: 1.5.3
3
+ Version: 1.5.4
4
4
  Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://mmif.clams.ai
@@ -21,7 +21,7 @@ Requires-Dist: pillow; extra == "cv"
21
21
  Requires-Dist: opencv-python; extra == "cv"
22
22
  Requires-Dist: ffmpeg-python; extra == "cv"
23
23
  Requires-Dist: wurlitzer; extra == "cv"
24
- Requires-Dist: av; extra == "cv"
24
+ Requires-Dist: av>=15; extra == "cv"
25
25
  Provides-Extra: seq
26
26
  Requires-Dist: numpy; extra == "seq"
27
27
  Provides-Extra: docs
@@ -1,4 +1,5 @@
1
1
  "``mmif-python`` version","Target MMIF Specification"
2
+ `1.5.4 <https://pypi.org/project/mmif-python/1.5.4/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
2
3
  `1.5.3 <https://pypi.org/project/mmif-python/1.5.3/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
4
  `1.5.2 <https://pypi.org/project/mmif-python/1.5.2/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
4
5
  `1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
@@ -6,6 +6,7 @@ In MMIF, annotations are created by apps in a workflow as a part
6
6
  of a view. For documentation on how views are represented, see
7
7
  :mod:`mmif.serialize.view`.
8
8
  """
9
+ import functools
9
10
  import importlib
10
11
  import itertools
11
12
  import os
@@ -22,7 +23,7 @@ from .model import MmifObject, PRMTV_TYPES
22
23
  from .. import DocumentTypes, AnnotationTypes
23
24
  import mmif_docloc_http
24
25
 
25
- __all__ = ['Annotation', 'AnnotationProperties', 'Document', 'DocumentProperties', 'Text']
26
+ __all__ = ['Annotation', 'AnnotationProperties', 'Document', 'DocumentProperties', 'Text', 'canonicalize_prop_name']
26
27
 
27
28
  T = TypeVar('T')
28
29
  LIST_PRMTV = typing.List[PRMTV_TYPES] # list of values (most cases for annotation props)
@@ -41,6 +42,40 @@ discovered_docloc_plugins.update({
41
42
  })
42
43
 
43
44
 
45
+ @functools.lru_cache(maxsize=None)
46
+ def _prop_alias_map(shortname: str) -> Dict[str, str]:
47
+ """
48
+ Maps every spelling of an aliased property of a type to the canonical
49
+ name. The table is read from the latest version of the type in the
50
+ vocabulary (``AnnotationTypes.<shortname>``), which is the single source
51
+ of truth for property aliases.
52
+
53
+ :param shortname: shortname of the annotation type (e.g. ``TimeFrame``)
54
+ :return: ``{spelling: canonical_name}``, empty for types without aliases
55
+ """
56
+ latest = getattr(AnnotationTypes, shortname, None)
57
+ groups = getattr(latest, '_property_aliases', None) or {}
58
+ return {alias: canonical for canonical, group in groups.items() for alias in group}
59
+
60
+
61
+ def canonicalize_prop_name(at_type: ThingTypesBase, prop_name: str) -> str:
62
+ """
63
+ Resolves a property name to its canonical spelling for the given type.
64
+ Property aliases were introduced in MMIF 1.0.2, when the general
65
+ ``label`` property replaced ``frameType`` (``TimeFrame``) and ``boxType``
66
+ (``BoundingBox``). Names that are not aliases, and types without an alias
67
+ table, are returned unchanged.
68
+
69
+ :param at_type: the ``@type`` of the annotation
70
+ :param prop_name: a property name, canonical or deprecated
71
+ :return: the canonical property name
72
+ """
73
+ shortname = getattr(at_type, 'shortname', None)
74
+ if not shortname:
75
+ return prop_name
76
+ return _prop_alias_map(shortname).get(prop_name, prop_name)
77
+
78
+
44
79
  class Annotation(MmifObject):
45
80
  """
46
81
  MmifObject that represents an annotation in a MMIF view.
@@ -49,11 +84,11 @@ class Annotation(MmifObject):
49
84
  def __init__(self, anno_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
50
85
  self._type: ThingTypesBase = ThingTypesBase('')
51
86
  # to store the parent view ID
52
- self._props_ephemeral: AnnotationProperties = AnnotationProperties()
87
+ self._props_ephemeral: AnnotationProperties = AnnotationProperties(None, self)
53
88
  self._alignments = {} # to hold alignment information (Alignment anno long_id -> aligned anno long_id)
54
89
  self.reserved_names.update(('_props_ephemeral', '_alignments'))
55
90
  if not hasattr(self, 'properties'): # don't overwrite DocumentProperties on super() call
56
- self.properties: AnnotationProperties = AnnotationProperties()
91
+ self.properties: AnnotationProperties = AnnotationProperties(None, self)
57
92
  self._attribute_classes = {'properties': AnnotationProperties}
58
93
  self.disallow_additional_properties()
59
94
  self._required_attributes = ["_type", "properties"]
@@ -69,9 +104,7 @@ class Annotation(MmifObject):
69
104
  # here is the place to parse formatted IDs and store prefixes in the parent mmif object.
70
105
  # (see https://github.com/clamsproject/mmif/issues/64#issuecomment-849241309 for discussion)
71
106
  super()._deserialize(input_dict)
72
- for k, v in self.properties.items():
73
- self._add_prop_aliases(k, v)
74
-
107
+
75
108
  def _cache_alignment(self, alignment_ann: 'Annotation', alignedto_ann: 'Annotation') -> None:
76
109
  """
77
110
  Cache alignment information. This cache will not be serialized.
@@ -107,29 +140,20 @@ class Annotation(MmifObject):
107
140
  yield aligned
108
141
 
109
142
 
110
- def _add_prop_aliases(self, key_to_add, val_to_add):
111
- """
112
- Method to handle aliases of the same property.
113
- Annotation property aliases were first introduced in MMIF 1.0.2,
114
- with addition of general `label` property to all `Annotation`
115
- subtypes, and effectively deprecated `frameType` and `boxType`
116
- in `TimeFrame` and `BoundingBox` respectively.
117
- """
118
- prop_aliases = AnnotationTypes._prop_aliases.get(self._type.shortname, {})
119
- for alias_rep, alias_group in prop_aliases.items():
120
- if key_to_add in alias_group:
121
- for alias in alias_group:
122
- if alias != key_to_add:
123
- self._props_ephemeral[alias] = val_to_add
124
- if alias in self.properties.keys():
125
- warning_msg = (f'Both "{key_to_add}" and "{alias}" are in the properties of "{self.id}", '
126
- f'however ')
127
- if alias == alias_rep:
128
- warning_msg += f'"{key_to_add}" is an alias of "{alias_rep}".'
129
- else:
130
- warning_msg += f'"{key_to_add}" and "{alias}" are both aliases of "{alias_rep}".'
131
- warning_msg += f'Having two synonyms in the same annotation can cause unexpected behavior. '
132
- warnings.warn(warning_msg, UserWarning)
143
+ def _add_ephemeral_defaults(self, defaults: typing.Mapping[str, typing.Any]) -> None:
144
+ """
145
+ Distributes view-level defaults (from the ``contains`` metadata of the
146
+ parent view) to this annotation as ephemeral properties, under their
147
+ canonical names. An annotation-level value takes precedence over a
148
+ view-level default, so a default is skipped when the annotation
149
+ carries the property itself.
150
+
151
+ :param defaults: property defaults for this annotation's ``@type``
152
+ """
153
+ for name, value in defaults.items():
154
+ name = canonicalize_prop_name(self._type, name)
155
+ if name not in self.properties:
156
+ self._props_ephemeral[name] = value
133
157
 
134
158
  def is_type(self, at_type: Union[str, ThingTypesBase]) -> bool:
135
159
  """
@@ -239,7 +263,9 @@ class Annotation(MmifObject):
239
263
  def add_property(self, name: str,
240
264
  value: Union[PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> None:
241
265
  """
242
- Adds a property to the annotation's properties.
266
+ Adds a property to the annotation's properties. A deprecated spelling
267
+ of an aliased property (e.g. ``frameType``) is stored under its
268
+ canonical name (``label``), with a :class:`DeprecationWarning`.
243
269
 
244
270
  :param name: the name of the property
245
271
  :param value: the property's desired value
@@ -252,14 +278,14 @@ class Annotation(MmifObject):
252
278
  # "either string, number, boolean, None, a JSON array of them, "
253
279
  # "or a JSON object of them keyed by strings."
254
280
  # f"(\"{name}\": \"{str(value)}\"")
255
- self._add_prop_aliases(name, value)
256
281
 
257
282
  def __getitem__(self, prop_name: str):
258
283
  if prop_name in {'at_type', '@type'}:
259
284
  return str(self._type)
260
285
  elif prop_name == 'properties':
261
286
  return self.properties
262
- elif prop_name in self.properties:
287
+ prop_name = canonicalize_prop_name(self._type, prop_name)
288
+ if prop_name in self.properties:
263
289
  return self.properties[prop_name]
264
290
  elif prop_name in self._props_ephemeral:
265
291
  return self._props_ephemeral[prop_name]
@@ -279,7 +305,9 @@ class Annotation(MmifObject):
279
305
  3. Special fields (``@type``, ``properties``)
280
306
 
281
307
  This allows convenient access to properties without explicitly
282
- checking the ``properties`` object or view-level metadata.
308
+ checking the ``properties`` object or view-level metadata. A
309
+ deprecated spelling of an aliased property (e.g. ``frameType``)
310
+ resolves to the canonical property (``label``).
283
311
 
284
312
  :param prop_name: The name of the property to retrieve
285
313
  :param default: The value to return if the property is not found (default: None)
@@ -318,23 +346,6 @@ class Annotation(MmifObject):
318
346
  except KeyError:
319
347
  return False
320
348
 
321
- def _get_label(self) -> str:
322
- """
323
- Another prototypical method to handle property aliases.
324
- See :meth:`.Annotation._add_prop_aliases` for more details on
325
- what property aliases are.
326
- Not recommended to use this method as `_add_prop_aliases` method
327
- is preferred.
328
- """
329
- if 'label' in self:
330
- return str(self.get('label'))
331
- elif self._type.shortname == 'TimeFrame' and 'frameType' in self:
332
- return str(self.get('frameType'))
333
- elif self._type.shortname == 'BoundingBox' and 'boxType' in self:
334
- return str(self.get('boxType'))
335
- else:
336
- raise KeyError("No label found in this annotation.")
337
-
338
349
  def is_document(self):
339
350
  return isinstance(self._type, DocumentTypesBase)
340
351
 
@@ -356,8 +367,8 @@ class Document(Annotation):
356
367
  # see https://github.com/clamsproject/mmif-python/issues/226 for discussion
357
368
  # around the use of these three dictionaries
358
369
  # (names changed since, `existing` >> `ephemeral` and `temporary` >> `pending`)
359
- self._props_original: DocumentProperties = DocumentProperties()
360
- self._props_pending: AnnotationProperties = AnnotationProperties()
370
+ self._props_original: DocumentProperties = DocumentProperties(None, self)
371
+ self._props_pending: AnnotationProperties = AnnotationProperties(None, self)
361
372
  self.reserved_names.update(('_props_original', '_props_pending'))
362
373
 
363
374
  self._type: Union[ThingTypesBase, DocumentTypesBase] = ThingTypesBase('')
@@ -578,6 +589,7 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
578
589
  """
579
590
 
580
591
  def __delitem__(self, key: str) -> None:
592
+ key = self._canonical(key)
581
593
  frm = None
582
594
  for k in self.__iter__():
583
595
  if k == key:
@@ -593,36 +605,83 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
593
605
  del frm[key]
594
606
  else:
595
607
  raise KeyError(f'Key "{key}" not found.')
596
-
608
+
597
609
  def __iter__(self) -> Iterator[str]:
598
610
  """
599
- ``__iter__`` on Mapping should basically work as ``keys()`` method
611
+ ``__iter__`` on Mapping should basically work as ``keys()`` method
600
612
  of vanilla dict.
601
613
  """
602
614
  for key in itertools.chain(self._named_attributes(), self._unnamed_attributes):
603
615
  yield key
604
-
616
+
605
617
  def __getitem__(self, key):
606
618
  """
607
- Parent MmifObject class has a __getitem__ method that checks if
608
- the value is empty when asked for an unnamed attribute. But for
609
- AnnotationProperties, any arbitrary property that's added
610
- explicitly by the user (developer) should not be ignored and
611
- returned even the value is empty.
619
+ Parent MmifObject class has a __getitem__ method that checks if
620
+ the value is empty when asked for an unnamed attribute. But for
621
+ AnnotationProperties, any arbitrary property that's added
622
+ explicitly by the user (developer) should not be ignored and
623
+ returned even the value is empty.
612
624
  """
625
+ key = self._canonical(key)
613
626
  if key in self._named_attributes():
614
627
  return self.__dict__[key]
615
628
  else:
616
629
  return self._unnamed_attributes[key]
617
630
 
618
- def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
631
+ def __setitem__(self, key: str, value) -> None:
632
+ """
633
+ Sets a property. A deprecated spelling of an aliased property (e.g.
634
+ ``frameType``) is stored under its canonical name (``label``), with
635
+ a :class:`DeprecationWarning`.
636
+ """
637
+ canonical = self._canonical(key)
638
+ if canonical != key:
639
+ warnings.warn(f'"{key}" is a deprecated alias of "{canonical}", storing the value as "{canonical}".',
640
+ DeprecationWarning)
641
+ super().__setitem__(canonical, value)
642
+
643
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None,
644
+ parent: Optional['Annotation'] = None, *_) -> None:
619
645
  self.id: str = ''
620
646
  # any individual at_type (subclassing this class) can have its own set of required attributes
621
647
  self._required_attributes = ["id"]
622
648
  # allowing additional attributes for arbitrary annotation properties
623
649
  self._unnamed_attributes = {}
650
+ # the Annotation these properties belong to; its `@type` decides which
651
+ # property names are aliases of each other (see `canonicalize_prop_name`)
652
+ self._parent_annotation = parent
653
+ self.reserved_names.add('_parent_annotation')
624
654
  super().__init__(mmif_obj)
625
655
 
656
+ def _canonical(self, key: str) -> str:
657
+ if self._parent_annotation is None:
658
+ return key
659
+ return canonicalize_prop_name(self._parent_annotation._type, key)
660
+
661
+ def _deserialize(self, input_dict: dict) -> None:
662
+ """
663
+ Deserializes the properties, rewriting deprecated spellings to their
664
+ canonical names so that there is one key per property. When two
665
+ spellings of the same property are present, the canonical one wins
666
+ (or the first one seen, when neither is canonical), with a warning.
667
+ """
668
+ canonical_dict = {}
669
+ # canonical name -> the deprecated spelling it was read from
670
+ from_alias = {}
671
+ for key, value in input_dict.items():
672
+ canonical = self._canonical(key)
673
+ if canonical in canonical_dict and (canonical != key or canonical in from_alias):
674
+ alias = key if canonical != key else from_alias[canonical]
675
+ warnings.warn(f'Both "{alias}" and "{canonical}" are in the properties of '
676
+ f'"{input_dict.get("id", "")}", but "{alias}" is an alias of "{canonical}". '
677
+ f'Keeping the value of "{canonical}".', UserWarning)
678
+ if canonical != key:
679
+ continue
680
+ canonical_dict[canonical] = value
681
+ if canonical != key:
682
+ from_alias[canonical] = key
683
+ super()._deserialize(canonical_dict)
684
+
626
685
 
627
686
  class DocumentProperties(AnnotationProperties):
628
687
  """
@@ -632,7 +691,8 @@ class DocumentProperties(AnnotationProperties):
632
691
  :param mmif_obj: the JSON data that defines the properties
633
692
  """
634
693
 
635
- def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
694
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None,
695
+ parent: Optional['Annotation'] = None, *_) -> None:
636
696
  self.mime: str = ''
637
697
  # note the trailing underscore here. I wanted to use the name `location`
638
698
  # for @property in this class and `Document` class, so had to use a diff
@@ -643,9 +703,9 @@ class DocumentProperties(AnnotationProperties):
643
703
  self._attribute_classes = {'text': Text}
644
704
  # in theory, either `location` or `text` should appear in a `document`
645
705
  # but with current implementation, there's no easy way to set a condition
646
- # for `oneOf` requirement
647
- # see MmifObject::_required_attributes in model.py
648
- super().__init__(mmif_obj)
706
+ # for `oneOf` requirement
707
+ # see MmifObject::_required_attributes in model.py
708
+ super().__init__(mmif_obj, parent)
649
709
 
650
710
  def _deserialize(self, input_dict: dict) -> None:
651
711
  if "location" in input_dict:
@@ -276,25 +276,11 @@ class Mmif(MmifObject):
276
276
  super()._deserialize(input_dict)
277
277
  for view in self.views:
278
278
  view._parent_mmif = self
279
- # this dict will be populated with properties
280
- # that are not encoded in individual annotations objects themselves
281
- extrinsic_props = defaultdict(dict)
282
- for at_type, type_lv_props in view.metadata.contains.items():
283
- for prop_key, prop_value in type_lv_props.items():
284
- extrinsic_props[at_type][prop_key] = prop_value
285
279
  for ann in view.get_annotations():
286
280
  ## for "capital" Annotation properties
287
- # first add all extrinsic properties to the Annotation objects
288
- # as "ephemeral" properties. A view-level `contains` default must
289
- # not override an annotation-level value that is already present
290
- # in `_props_ephemeral` -- namely an alias of a property set on the
291
- # annotation itself (put there by `_add_prop_aliases` during the
292
- # annotation's own deserialization, which runs earlier). Per the
293
- # spec, the annotation-level value takes precedence over the
294
- # view-level default.
295
- for prop_key, prop_value in extrinsic_props[ann.at_type].items():
296
- if prop_key not in ann._props_ephemeral:
297
- ann._props_ephemeral[prop_key] = prop_value
281
+ # first, distribute view-level defaults from `contains` metadata
282
+ # to the Annotation objects as "ephemeral" properties
283
+ ann._add_ephemeral_defaults(view.metadata.contains.get(ann.at_type, {}))
298
284
  # then, do the same to associated Document objects. Note that,
299
285
  # in a view, it is guaranteed that all Annotation objects are not duplicates
300
286
  if ann.at_type == AnnotationTypes.Annotation:
@@ -17,7 +17,7 @@ from datetime import datetime
17
17
  from typing import Dict, Union, Optional, Generator, List, cast
18
18
 
19
19
  from mmif import DocumentTypes, AnnotationTypes, ThingTypesBase, ClamsTypesBase
20
- from mmif.serialize.annotation import Annotation, Document
20
+ from mmif.serialize.annotation import Annotation, Document, canonicalize_prop_name
21
21
  from mmif.serialize.model import PRMTV_TYPES, MmifObject, DataList, DataDict
22
22
  from mmif.vocabulary.base_types import AnnotationTypesBase
23
23
 
@@ -197,8 +197,6 @@ class View(MmifObject):
197
197
  self._set_ann_id(new_annotation, aid)
198
198
  for propk, propv in properties.items():
199
199
  new_annotation.add_property(propk, propv)
200
- for propk, propv in self.metadata.contains.get(at_type, {}).items():
201
- new_annotation._props_ephemeral[propk] = propv
202
200
  return self.add_annotation(new_annotation, overwrite)
203
201
 
204
202
  def add_annotation(self, annotation: 'Annotation', overwrite=False) -> 'Annotation':
@@ -222,6 +220,7 @@ class View(MmifObject):
222
220
  annotation.id = f"{self.id}{self.id_delimiter}{annotation.id}"
223
221
  self.annotations.append(annotation, overwrite)
224
222
  self.new_contain(annotation.at_type)
223
+ annotation._add_ephemeral_defaults(self.metadata.contains.get(annotation.at_type, {}))
225
224
  if annotation.at_type == AnnotationTypes.Alignment:
226
225
  self._parent_mmif._cache_alignment(annotation)
227
226
  return annotation
@@ -280,13 +279,14 @@ class View(MmifObject):
280
279
  must match. Note that annotation type metadata are specified in the `contains` view metadata, not in individual \
281
280
  annotation objects.
282
281
  """
283
- def prop_check(k, v, *props):
284
- return any(k in prop and prop[k] == v for prop in props)
282
+ def prop_check(k, v, annotation, at_type_metadata):
283
+ k = canonicalize_prop_name(annotation.at_type, k)
284
+ return any(k in prop and prop[k] == v for prop in (annotation.properties, at_type_metadata))
285
285
 
286
286
  for annotation in self.annotations:
287
287
  at_type_metadata = self.metadata.contains.get(annotation.at_type, {})
288
288
  if not at_type or (at_type and annotation.at_type == at_type):
289
- if all(map(lambda kv: prop_check(kv[0], kv[1], annotation.properties, at_type_metadata),
289
+ if all(map(lambda kv: prop_check(kv[0], kv[1], annotation, at_type_metadata),
290
290
  properties.items())):
291
291
  yield annotation
292
292
 
@@ -485,7 +485,7 @@ class ViewMetadata(MmifObject):
485
485
  at_type = ThingTypesBase.from_str(at_type)
486
486
 
487
487
  if at_type not in self.contains:
488
- new_contain = Contain(contains_metadata)
488
+ new_contain = Contain._for_type(at_type, contains_metadata)
489
489
  self.add_contain(new_contain, at_type)
490
490
  return new_contain
491
491
 
@@ -563,6 +563,15 @@ class Contain(DataDict[str, str]):
563
563
  annotation type in the ``contains`` metadata of a MMIF view.
564
564
  """
565
565
 
566
+ @classmethod
567
+ def _for_type(cls, at_type: ThingTypesBase, contains_metadata: dict) -> 'Contain':
568
+ """
569
+ Builds a ``Contain`` for the given ``@type``, with property names
570
+ rewritten to their canonical spellings (see
571
+ :func:`mmif.serialize.annotation.canonicalize_prop_name`).
572
+ """
573
+ return cls({canonicalize_prop_name(at_type, k): v for k, v in contains_metadata.items()})
574
+
566
575
 
567
576
  class AnnotationsList(DataList[Union[Annotation, Document]]):
568
577
  """
@@ -608,7 +617,7 @@ class ContainsDict(DataDict[ThingTypesBase, Contain]):
608
617
  for key, value in input_dict.items():
609
618
  if isinstance(key, str):
610
619
  key = ThingTypesBase.from_str(key)
611
- self._items[key] = Contain(value)
620
+ self._items[key] = Contain._for_type(key, value)
612
621
 
613
622
  def update(self, other: Union[dict, 'ContainsDict'], overwrite=False):
614
623
  for k, v in other.items():
@@ -1,10 +1,64 @@
1
1
  import json
2
+ from collections.abc import MutableMapping
2
3
 
3
4
  from typing import Any
4
5
 
6
+ from mmif.serialize.annotation import canonicalize_prop_name
7
+ from mmif.serialize.model import MmifObject
5
8
  from mmif.utils.summarizer import config
6
9
 
7
10
 
11
+ class NodeProperties(MutableMapping):
12
+
13
+ """Read-through view of an annotation's properties for use in a Node. Reads
14
+ resolve, in order, local writes made by the summarizer, the annotation's own
15
+ properties (deprecated spellings included, e.g. `word` for `text`), and the
16
+ view-level defaults for the annotation's type in the view's `contains`
17
+ metadata. Values are plain JSON types, as they would be after a serialization
18
+ round trip. Writes never reach the annotation; they are kept locally so that
19
+ summarizing leaves the loaded MMIF unchanged.
20
+
21
+ Note on the alternative: the SDK could instead copy `contains` defaults into
22
+ each annotation's `.properties` at deserialization and rely on
23
+ `Mmif.factor_out_shared_properties` to lift them back out on serialization.
24
+ That round trip is not shape-preserving when a type has a single annotation
25
+ in the view, when the key is non-factorable (`start`, `source`, `target`, ...),
26
+ or when another annotation of the type overrides the default, so the SDK
27
+ keeps defaults out of `.properties` and this class resolves them here."""
28
+
29
+ def __init__(self, annotation, view):
30
+ self.annotation = annotation
31
+ contains = {} if view is None else view.metadata.contains.get(annotation.at_type, {})
32
+ self.defaults = {canonicalize_prop_name(annotation.at_type, k): v for k, v in contains.items()}
33
+ self.local = {}
34
+
35
+ @staticmethod
36
+ def _as_json(value):
37
+ return json.loads(str(value)) if isinstance(value, MmifObject) else value
38
+
39
+ def __getitem__(self, key):
40
+ if key in self.local:
41
+ return self.local[key]
42
+ if key in self.annotation.properties:
43
+ return self._as_json(self.annotation.properties[key])
44
+ return self.defaults[canonicalize_prop_name(self.annotation.at_type, key)]
45
+
46
+ def __setitem__(self, key, value):
47
+ self.local[key] = value
48
+
49
+ def __delitem__(self, key):
50
+ del self.local[key]
51
+
52
+ def __iter__(self):
53
+ seen = set()
54
+ for key in (*self.annotation.properties, *self.defaults, *self.local):
55
+ if key not in seen:
56
+ seen.add(key)
57
+ yield key
58
+
59
+ def __len__(self):
60
+ return sum(1 for _ in self)
61
+
8
62
 
9
63
  class Node(object):
10
64
 
@@ -16,7 +70,7 @@ class Node(object):
16
70
  # copy some information from the Annotation
17
71
  self.at_type = annotation.at_type
18
72
  self.identifier = annotation.id
19
- self.properties = json.loads(str(annotation.properties))
73
+ self.properties = NodeProperties(annotation, view)
20
74
  # get the document from the view or the properties
21
75
  self.document = self._get_document()
22
76
  # The targets property contains a list of annotations or documents that
@@ -151,23 +205,10 @@ class Node(object):
151
205
 
152
206
  def _get_document(self):
153
207
  """Return the document or annotation node that the annotation/document in
154
- the node refers to via the document property. This could be a local property
155
- or a metadata property if there is no such local property. Return None
156
- if neither of those exist."""
157
- # try the local property
208
+ the node refers to via the document property (the annotation's own, or the
209
+ view-level default). Return None if there is none."""
158
210
  docid = self.properties.get('document')
159
- if docid is not None:
160
- # print('>>>', docid, self.graph.get_node(docid))
161
- return self.graph.get_node(docid)
162
- # try the metadata property
163
- if self.view is not None:
164
- try:
165
- metadata = self.view.metadata.contains[self.at_type]
166
- docid = metadata['document']
167
- return self.graph.get_node(docid)
168
- except KeyError:
169
- return None
170
- return None
211
+ return None if docid is None else self.graph.get_node(docid)
171
212
 
172
213
  def summary(self):
173
214
  """The default summary is just the identfier, this should typically be
@@ -197,9 +238,9 @@ class Node(object):
197
238
  class TimeFrameNode(Node):
198
239
 
199
240
  def __str__(self):
200
- frame_type = ' ' + self.frame_type() if self.has_label() else ''
241
+ label = f' {self.label()}' if self.has_label() else ''
201
242
  return ('<TimeFrameNode %s %s:%s%s>'
202
- % (self.identifier, self.start(), self.end(), frame_type))
243
+ % (self.identifier, self.start(), self.end(), label))
203
244
 
204
245
  def start(self):
205
246
  return self.properties.get('start', -1)
@@ -207,13 +248,11 @@ class TimeFrameNode(Node):
207
248
  def end(self):
208
249
  return self.properties.get('end', -1)
209
250
 
210
- def frame_type(self):
211
- # TODO: rename this, uses old property since replaced by "label""
212
- # NOTE: this is still aloowing for the old property though
213
- return self.properties.get('label') or self.properties.get('frameType')
251
+ def label(self):
252
+ return self.properties.get('label')
214
253
 
215
254
  def has_label(self):
216
- return self.frame_type() is not None
255
+ return self.label() is not None
217
256
 
218
257
  def representatives(self) -> list:
219
258
  """Return a list of the representative TimePoints."""
@@ -228,7 +267,7 @@ class TimeFrameNode(Node):
228
267
  return { 'id': self.identifier,
229
268
  'start': self.properties['start'],
230
269
  'end': self.properties['end'],
231
- 'frameType': self.properties.get('frameType') }
270
+ 'frameType': self.label() }
232
271
 
233
272
 
234
273
  class EntityNode(Node):
@@ -147,7 +147,7 @@ class Annotations(object):
147
147
  self.data = defaultdict(list)
148
148
  # summary.graph.get_nodes(config.ANNOTATION, view_id=view.id)
149
149
  for anno in summary.graph.get_nodes(config.ANNOTATION):
150
- self.data[anno.view.id].append(anno.properties)
150
+ self.data[anno.view.id].append(dict(anno.properties))
151
151
 
152
152
  def get(self, item):
153
153
  return self.data.get(item, [])
@@ -300,7 +300,7 @@ class TranscriptElement:
300
300
  # this adds the current token to the transcript
301
301
  start = t.properties['start']
302
302
  end = t.properties['end']
303
- word = t.properties['word']
303
+ word = t.properties['text']
304
304
  transcript.set_chars(word, start, end)
305
305
  self.id = identifier
306
306
  self.start = sentence[0].anchors['time-offsets'][0]
@@ -378,7 +378,7 @@ class TimeFrames(Nodes):
378
378
 
379
379
  def _collect_timeframe_summaries(self):
380
380
  for tf in self.nodes:
381
- label = tf.frame_type()
381
+ label = tf.label()
382
382
  try:
383
383
  start, end = tf.anchors['time-offsets']
384
384
  except KeyError: