Skip to content

Commit 1c10dca

Browse files
author
notactuallyfinn
committed
added coments and fix small bug
1 parent bcdc821 commit 1c10dca

7 files changed

Lines changed: 280 additions & 78 deletions

File tree

src/hermes/model/api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# SPDX-FileCopyrightText: 2026 German Aerospace Center (DLR)
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# SPDX-FileContributor: Michael Fritzsche
6+
# SPDX-FileContributor: Stephan Druskat
7+
18
from hermes.model.context_manager import HermesContext, HermesContexError
29
from hermes.model.types import ld_dict
310
from hermes.model.types.ld_context import ALL_CONTEXTS

src/hermes/model/merge/action.py

Lines changed: 230 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,81 +3,282 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
# SPDX-FileContributor: Michael Meinel
6+
# SPDX-FileContributor: Michael Fritzsche
67

7-
from hermes.model.types import ld_list
8+
from __future__ import annotations
9+
10+
from typing import TYPE_CHECKING, Callable, Union
11+
from typing_extensions import Self
12+
13+
from ..types import ld_dict, ld_list
14+
from ..types.ld_container import BASIC_TYPE, JSON_LD_VALUE, TIME_TYPE
15+
16+
if TYPE_CHECKING:
17+
from .container import ld_merge_dict, ld_merge_list
818

919

1020
class MergeError(ValueError):
21+
""" Class for any error while merging. """
1122
pass
1223

1324

1425
class MergeAction:
15-
def merge(self, target, key, value, update):
26+
""" Base class for the different actions occuring druing a merge. """
27+
def merge(
28+
self: Self,
29+
target: ld_merge_dict,
30+
key: list[Union[str, int]],
31+
value: ld_merge_list,
32+
update: Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
33+
) -> Union[JSON_LD_VALUE, BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]:
34+
"""
35+
An abstract method that needs to be implemented by all subclasses
36+
to have a generic way to use the merge actions.
37+
38+
:param target: The ld_merge_dict inside of which the items are merged.
39+
:type target: ld_merge_dict
40+
:param key: The "path" of keys so that parent[key[-1]] is value and
41+
for the outermost parent of target out_parent out_parent[key[0]]...[key[-1]] results in value.
42+
:type key: list[str | int]
43+
:param value: The value inside target that is to be merged with update.
44+
:type value: ld_merge_list
45+
:param update: The value that is to be merged into target with value.
46+
:type update: BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
47+
48+
:return: The merged value in an arbitrary format that is supported by :meth:`ld_dict.__setitem__`.
49+
:rtype: JSON_LD_VALUE | BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
50+
"""
1651
raise NotImplementedError()
1752

1853

1954
class Reject(MergeAction):
20-
@classmethod
21-
def merge(cls, target, key, value, update):
55+
def merge(
56+
self: Self,
57+
target: ld_merge_dict,
58+
key: list[Union[str, int]],
59+
value: ld_merge_list,
60+
update: Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
61+
) -> ld_merge_list:
62+
"""
63+
Rejects the new data ``update`` and lets target add an entry to itself documenting what data has been rejected.
64+
65+
:param target: The ld_merge_dict inside of which the items are merged.
66+
:type target: ld_merge_dict
67+
:param key: The "path" of keys so that parent[key[-1]] is value and
68+
for the outermost parent of target out_parent out_parent[key[0]]...[key[-1]] results in value.
69+
:type key: list[str | int]
70+
:param value: The value inside target that is to be merged with update.<br> This value won't be changed.
71+
:type value: ld_merge_list
72+
:param update: The value that is to be merged into target with value.<br> This value will be rejected.
73+
:type update: BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
74+
75+
:return: The merged value.<br>
76+
This value will always be value.
77+
:rtype: ld_merge_list
78+
"""
79+
# If necessary, add the entry that data has been rejected.
2280
if value != update:
2381
target.reject(key, update)
82+
# Return value unchanged.
2483
return value
2584

2685

2786
class Replace(MergeAction):
28-
@classmethod
29-
def merge(cls, target, key, value, update):
87+
def merge(
88+
self: Self,
89+
target: ld_merge_dict,
90+
key: list[Union[str, int]],
91+
value: ld_merge_list,
92+
update: Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
93+
) -> Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]:
94+
"""
95+
Replaces the old data ``value`` with the new data ``update``
96+
and lets target add an entry to itself documenting what data has been replaced.
97+
98+
:param target: The ld_merge_dict inside of which the items are merged.
99+
:type target: ld_merge_dict
100+
:param key: The "path" of keys so that parent[key[-1]] is value and
101+
for the outermost parent of target out_parent out_parent[key[0]]...[key[-1]] results in value.
102+
:type key: list[str | int]
103+
:param value: The value inside target that is to be merged with update.<br> This value will bew replaced.
104+
:type value: ld_merge_list
105+
:param update: The value that is to be merged into target with value.<br>
106+
This value will be used instead of value.
107+
:type update: BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
108+
109+
:return: The merged value.<br>
110+
This value will be update.
111+
:rtype: BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
112+
"""
113+
# If necessary, add the entry that data has been replaced.
30114
if value != update:
31115
target.replace(key, value)
116+
# Return the new value.
32117
return update
33118

34119

35120
class Concat(MergeAction):
36-
@classmethod
37-
def merge(cls, target, key, value, update):
38-
return cls.merge_to_list(value, update)
39-
40-
@classmethod
41-
def merge_to_list(cls, head, tail):
42-
if not isinstance(head, (list, ld_list)):
43-
head = [head]
44-
if not isinstance(tail, (list, ld_list)):
45-
head.append(tail)
121+
def merge(
122+
self: Self,
123+
target: ld_merge_dict,
124+
key: list[Union[str, int]],
125+
value: ld_merge_list,
126+
update: Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
127+
) -> ld_merge_list:
128+
"""
129+
Concatenates the new data ``update`` to the old data ``value``.
130+
131+
:param target: The ld_merge_dict inside of which the items are merged.
132+
:type target: ld_merge_dict
133+
:param key: The "path" of keys so that parent[key[-1]] is value and
134+
for the outermost parent of target out_parent out_parent[key[0]]...[key[-1]] results in value.
135+
:type key: list[str | int]
136+
:param value: The value inside target that is to be merged with update.
137+
:type value: ld_merge_list
138+
:param update: The value that is to be merged into target with value.
139+
:type update: BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
140+
141+
:return: The merged value.<br>
142+
``value`` concatenated with ``update``.
143+
:rtype: ld_merge_list
144+
"""
145+
# Concatenate the items and return the result.
146+
if isinstance(update, (list, ld_list)):
147+
value.extend(update)
46148
else:
47-
head.extend(tail)
48-
return head
149+
value.append(update)
150+
return value
49151

50152

51153
class Collect(MergeAction):
52-
def __init__(self, match):
154+
def __init__(
155+
self: Self,
156+
match: Union[
157+
Callable[
158+
[
159+
Union[BASIC_TYPE, TIME_TYPE, ld_merge_dict, ld_merge_list],
160+
Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
161+
],
162+
bool
163+
],
164+
Callable[[ld_merge_dict, ld_dict], bool]
165+
]
166+
) -> None:
167+
"""
168+
Set the match function for this collect merge action.
169+
170+
:param match: The function used to evaluate equality while merging.
171+
:type match: Callable[
172+
[BASIC_TYPE | TIME_TYPE | ld_merge_dict | ld_merge_list, BASIC_TYPE | TIME_TYPE | ld_dict | ld_list],
173+
bool
174+
] | Callable[[ld_merge_dict, ld_dict], bool]
175+
176+
:return:
177+
:rtype: None
178+
"""
53179
self.match = match
54180

55-
def merge(self, target, key, value, update):
56-
if not isinstance(value, list):
57-
value = [value]
58-
if not isinstance(update, list):
181+
def merge(
182+
self: Self,
183+
target: ld_merge_dict,
184+
key: list[Union[str, int]],
185+
value: ld_merge_list,
186+
update: Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
187+
) -> ld_merge_list:
188+
"""
189+
Collects the unique items (according to :attr:`match`) from ``value`` and ``update``.
190+
191+
:param target: The ld_merge_dict inside of which the items are merged.
192+
:type target: ld_merge_dict
193+
:param key: The "path" of keys so that parent[key[-1]] is value and
194+
for the outermost parent of target out_parent out_parent[key[0]]...[key[-1]] results in value.
195+
:type key: list[str | int]
196+
:param value: The value inside target that is to be merged with update.
197+
:type value: ld_merge_list
198+
:param update: The value that is to be merged into target with value.
199+
:type update: BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
200+
201+
:return: The merged value.
202+
:rtype: ld_merge_list
203+
"""
204+
if not isinstance(update, (list, ld_list)):
59205
update = [update]
60206

207+
# iterate over all new items
61208
for update_item in update:
209+
# If the current new item has no occurence in value (according to self.match) add it to value.
62210
if not any(self.match(item, update_item) for item in value):
63211
value.append(update_item)
64212

65-
if len(value) == 1:
66-
return value[0]
67-
else:
68-
return value
213+
return value
69214

70215

71216
class MergeSet(MergeAction):
72-
def __init__(self, match, merge_items=True):
217+
def __init__(
218+
self: Self,
219+
match: Union[
220+
Callable[
221+
[
222+
Union[BASIC_TYPE, TIME_TYPE, ld_merge_dict, ld_merge_list],
223+
Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
224+
],
225+
bool
226+
],
227+
Callable[[ld_merge_dict, ld_dict], bool]
228+
],
229+
merge_items: bool = True
230+
) -> None:
231+
"""
232+
Set the match function for this collect merge action.
233+
234+
:param match: The function used to evaluate equality while merging.
235+
:type match: Callable[
236+
[BASIC_TYPE | TIME_TYPE | ld_merge_dict | ld_merge_list, BASIC_TYPE | TIME_TYPE | ld_dict | ld_list],
237+
bool
238+
] | Callable[[ld_merge_dict, ld_dict], bool]
239+
:param merge_items: Whether or to to merge similar items. (If false this is basically :class:`Concat`)
240+
:type merge_items: bool
241+
242+
:return:
243+
:rtype: None
244+
"""
73245
self.match = match
74246
self.merge_items = merge_items
75247

76-
def merge(self, target, key, value, update):
248+
def merge(
249+
self: Self,
250+
target: ld_merge_dict,
251+
key: list[Union[str, int]],
252+
value: ld_merge_list,
253+
update: Union[BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
254+
) -> ld_merge_list:
255+
"""
256+
Merges similar items (according to :attr:`match`) from ``value`` and ``update``.
257+
258+
:param target: The ld_merge_dict inside of which the items are merged.
259+
:type target: ld_merge_dict
260+
:param key: The "path" of keys so that parent[key[-1]] is value and
261+
for the outermost parent of target out_parent out_parent[key[0]]...[key[-1]] results in value.
262+
:type key: list[str | int]
263+
:param value: The value inside target that is to be merged with update.
264+
:type value: ld_merge_list
265+
:param update: The value that is to be merged into target with value.
266+
:type update: BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
267+
268+
:return: The merged value.
269+
:rtype: ld_merge_list
270+
"""
271+
if not isinstance(update, (list, ld_list)):
272+
update = [update]
273+
77274
for item in update:
275+
# For each new item merge it into a similar item (according to match) inside target[key[-1]]
276+
# (aka inside value) if such an item exists and merging is permitted.
277+
# Otherwise append it to target[key[-1]] (aka to value).
78278
target_item = target.match(key[-1], item, self.match)
79279
if target_item and self.merge_items:
80280
target_item.update(item)
81281
else:
82282
value.append(item)
283+
# Return the merged values.
83284
return value

src/hermes/model/merge/container.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
# SPDX-FileContributor: Michael Meinel
66
# SPDX-FileContributor: Michael Fritzsche
77

8-
from typing import Callable, Union
8+
from __future__ import annotations
9+
10+
from typing import Callable, Union, TYPE_CHECKING
911
from typing_extensions import Self
1012

11-
from hermes.model.merge.action import MergeAction
12-
from hermes.model.types import ld_container, ld_context, ld_dict, ld_list
13-
from hermes.model.types.ld_container import (
13+
from ..types import ld_container, ld_context, ld_dict, ld_list
14+
from ..types.ld_container import (
1415
BASIC_TYPE, EXPANDED_JSON_LD_VALUE, JSON_LD_CONTEXT_DICT, JSON_LD_VALUE, TIME_TYPE
1516
)
16-
17-
from .strategy import CODEMETA_STRATEGY, PROV_STRATEGY, REPLACE_STRATEGY
1817
from ..types.pyld_util import bundled_loader
18+
from .strategy import CODEMETA_STRATEGY, PROV_STRATEGY, REPLACE_STRATEGY
19+
20+
if TYPE_CHECKING:
21+
from .action import MergeAction
1922

2023

2124
class _ld_merge_container:
@@ -170,24 +173,12 @@ def update_context(
170173
:rtype: None
171174
"""
172175
if other_context:
173-
if len(self.context) < 1 or not isinstance(self.context[-1], dict):
174-
self.context.append({})
175-
176-
if not isinstance(other_context, list):
177-
other_context = [other_context]
178-
for ctx in other_context:
179-
if isinstance(ctx, dict):
180-
# FIXME #471: Shouldn't the dict be appended instead?
181-
# How it is implemented currently results in anomalies like this:
182-
# other_context = [{"codemeta": "https://doi.org/10.5063/schema/codemeta-1.0/"}]
183-
# self.context = [{"codemeta": "https://doi.org/10.5063/schema/codemeta-2.0/"}]
184-
# resulting context is only [{"codemeta": "https://doi.org/10.5063/schema/codemeta-1.0/"}]
185-
# values that start with "https://doi.org/10.5063/schema/codemeta-2.0/" can't be compacted anymore
186-
self.context[-1].update(ctx)
187-
elif ctx not in self.context:
188-
# FIXME #471: If multiple string values are in self.context, the others are prefered
189-
# if the new one is inserted at the beginning. But with the dictionaries the order is reversed.
190-
self.context.insert(0, ctx)
176+
if not isinstance(self.context, list):
177+
self.context = [self.context]
178+
if isinstance(other_context, list):
179+
self.context = [*other_context, *self.context]
180+
else:
181+
self.context = [other_context, *self.context]
191182

192183
# update the active context that is used for compaction/ expansion
193184
self.active_ctx = self.ld_proc.initial_ctx(self.context, {"documentLoader": bundled_loader})
@@ -270,10 +261,7 @@ def match(
270261
:type value: Union[JSON_LD_VALUE, BASIC_TYPE, TIME_TYPE, ld_dict, ld_list]
271262
:param match: The method defining if two objects are a match.
272263
:type match: Callable[
273-
[
274-
BASIC_TYPE | TIME_TYPE | ld_merge_dict | ld_merge_list,
275-
BASIC_TYPE | TIME_TYPE | ld_dict | ld_list
276-
],
264+
[BASIC_TYPE | TIME_TYPE | ld_merge_dict | ld_merge_list, BASIC_TYPE | TIME_TYPE | ld_dict | ld_list],
277265
bool
278266
] | Callable[[ld_merge_dict, ld_dict], bool]
279267

0 commit comments

Comments
 (0)