Skip to content

Commit 94a55db

Browse files
authored
Merge pull request #44 from Tiro-health/master
Support `collection.abc.Mapping` as resource instead of only `dict`
2 parents 77e6992 + eb747dd commit 94a55db

File tree

6 files changed

+133
-10
lines changed

6 files changed

+133
-10
lines changed

Diff for: fhirpathpy/engine/evaluators/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import abc
12
from decimal import Decimal
23
from functools import reduce
34

@@ -185,7 +186,7 @@ def func(acc, res):
185186
if isinstance(res.data, nodes.FP_Quantity):
186187
toAdd = res.data.value
187188

188-
if actualTypes and isinstance(res.data, dict):
189+
if actualTypes and isinstance(res.data, abc.Mapping):
189190
# Use actualTypes to find the field's value
190191
for actualType in actualTypes:
191192
field = f"{key}{actualType}"
@@ -194,7 +195,7 @@ def func(acc, res):
194195
if toAdd is not None or toAdd_ is not None:
195196
childPath += actualType
196197
break
197-
elif isinstance(res.data, dict):
198+
elif isinstance(res.data, abc.Mapping):
198199
toAdd = res.data.get(key)
199200
toAdd_ = res.data.get(f"_{key}")
200201
if key == "extension":

Diff for: fhirpathpy/engine/invocations/equality.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import abc
12
from decimal import Decimal
23
import json
34
import fhirpathpy.engine.util as util
@@ -80,9 +81,9 @@ def equivalence(ctx, x, y):
8081
if isinstance(x_val, nodes.FP_Quantity) and isinstance(y_val, nodes.FP_Quantity):
8182
return x_val.deep_equal(y_val)
8283

83-
if isinstance(a, (dict, list)) and isinstance(b, (dict, list)):
84+
if isinstance(a, (abc.Mapping, list)) and isinstance(b, (abc.Mapping, list)):
8485
def deep_equal(a, b):
85-
if isinstance(a, dict) and isinstance(b, dict):
86+
if isinstance(a, abc.Mapping) and isinstance(b, abc.Mapping):
8687
if a.keys() != b.keys():
8788
return False
8889
return all(deep_equal(a[key], b[key]) for key in a)

Diff for: fhirpathpy/engine/invocations/filtering.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import abc
12
from decimal import Decimal
23
import numbers
34
import fhirpathpy.engine.util as util
@@ -121,7 +122,7 @@ def extension(ctx, data, url):
121122
res = []
122123
for d in data:
123124
element = util.get_data(d)
124-
if isinstance(element, dict):
125+
if isinstance(element, abc.Mapping):
125126
exts = [e for e in element.get("extension", []) if e["url"] == url]
126127
if len(exts) > 0:
127128
res.append(nodes.ResourceNode.create_node(exts[0], "Extension"))

Diff for: fhirpathpy/engine/invocations/navigation.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import abc
12
from functools import reduce
23
import fhirpathpy.engine.util as util
34
import fhirpathpy.engine.nodes as nodes
@@ -15,7 +16,7 @@ def func(acc, res):
1516
if isinstance(data, list):
1617
data = dict((i, data[i]) for i in range(0, len(data)))
1718

18-
if isinstance(data, dict):
19+
if isinstance(data, abc.Mapping):
1920
for prop in data.keys():
2021
value = data[prop]
2122
childPath = ""

Diff for: fhirpathpy/engine/nodes.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import abc
12
import copy
23
from datetime import datetime, timedelta, timezone
34
from dateutil.relativedelta import relativedelta
@@ -824,7 +825,7 @@ def __init__(self, data, path, _data=None):
824825
If data is a resource (maybe a contained resource) reset the path
825826
information to the resource type.
826827
"""
827-
if isinstance(data, dict) and "resourceType" in data:
828+
if isinstance(data, abc.Mapping) and "resourceType" in data:
828829
path = data["resourceType"]
829830

830831
self.path = path
@@ -872,7 +873,7 @@ def convert_data(self):
872873
cls = TypeInfo.type_to_class_with_check_string.get(self.path)
873874
if cls:
874875
data = FP_TimeBase.check_string(cls, data) or data
875-
if isinstance(data, dict) and data["system"] == "http://unitsofmeasure.org":
876+
if isinstance(data, abc.Mapping) and data["system"] == "http://unitsofmeasure.org":
876877
data = FP_Quantity(
877878
data["value"],
878879
FP_Quantity.timeUnitsToUCUM.get(data["code"], "'" + data["code"] + "'"),
@@ -934,7 +935,7 @@ def create_by_value_in_namespace(namespace, value):
934935
name = "Quantity"
935936
elif isinstance(value, str):
936937
name = "string"
937-
elif isinstance(value, dict):
938+
elif isinstance(value, abc.Mapping):
938939
name = "object"
939940

940941
if name == "bool":

Diff for: tests/test_evaluators.py

+119-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import json
2+
from collections.abc import Mapping
3+
from dataclasses import dataclass, fields
24
from datetime import datetime, timezone
3-
from unittest import mock
45
from decimal import Decimal
6+
from unittest import mock
7+
58
import pytest
69

710
from fhirpathpy import evaluate
@@ -275,3 +278,118 @@ def combining_functions_test(resource, path, expected):
275278
)
276279
def path_functions_test(resource, path, expected):
277280
assert evaluate(resource, path) == expected
281+
282+
283+
284+
@dataclass(eq=True)
285+
class NestedMapping(Mapping):
286+
d: int
287+
e: str
288+
289+
def __iter__(self):
290+
return iter([field.name for field in fields(self)])
291+
292+
def __getitem__(self, key: str):
293+
try:
294+
return getattr(self, key)
295+
except AttributeError:
296+
raise KeyError(key)
297+
298+
def __len__(self):
299+
return len(fields(self))
300+
301+
302+
@dataclass(eq=True)
303+
class CustomMapping(Mapping):
304+
a: int
305+
b: dict
306+
c: NestedMapping
307+
d: tuple[NestedMapping, ...]
308+
e: list[NestedMapping]
309+
310+
def __iter__(self):
311+
return iter([field.name for field in fields(self)])
312+
313+
def __getitem__(self, key: str):
314+
try:
315+
return getattr(self, key)
316+
except AttributeError:
317+
raise KeyError(key)
318+
319+
def __len__(self):
320+
return len(fields(self))
321+
322+
323+
@pytest.mark.parametrize(
324+
("resource", "path", "expected"),
325+
[
326+
(
327+
CustomMapping(
328+
a=1,
329+
b={"c": True},
330+
c=NestedMapping(d=3, e="f"),
331+
d=(NestedMapping(d=4, e="x"), NestedMapping(d=3, e="y")),
332+
e=[NestedMapping(d=4, e="z"), NestedMapping(d=5, e="w")],
333+
),
334+
"a",
335+
[1],
336+
),
337+
(
338+
CustomMapping(
339+
a=1,
340+
b={"c": True},
341+
c=NestedMapping(d=3, e="f"),
342+
d=(NestedMapping(d=4, e="x"), NestedMapping(d=3, e="y")),
343+
e=[NestedMapping(d=4, e="z"), NestedMapping(d=5, e="w")],
344+
),
345+
"b.c",
346+
[True],
347+
),
348+
(
349+
CustomMapping(
350+
a=1,
351+
b={"c": True},
352+
c=NestedMapping(d=3, e="f"),
353+
d=(NestedMapping(d=4, e="x"), NestedMapping(d=3, e="y")),
354+
e=[NestedMapping(d=4, e="z"), NestedMapping(d=5, e="w")],
355+
),
356+
"c",
357+
[NestedMapping(d=3, e="f")],
358+
),
359+
(
360+
CustomMapping(
361+
a=1,
362+
b={"c": True},
363+
c=NestedMapping(d=3, e="f"),
364+
d=(NestedMapping(d=4, e="x"), NestedMapping(d=3, e="y")),
365+
e=[NestedMapping(d=4, e="z"), NestedMapping(d=5, e="w")],
366+
),
367+
"c.d",
368+
[3],
369+
),
370+
(
371+
CustomMapping(
372+
a=1,
373+
b={"c": True},
374+
c=NestedMapping(d=3, e="f"),
375+
d=(NestedMapping(d=4, e="x"), NestedMapping(d=3, e="y")),
376+
e=[NestedMapping(d=4, e="z"), NestedMapping(d=5, e="w")],
377+
),
378+
"e.last()",
379+
[NestedMapping(d=5, e="w")],
380+
),
381+
(
382+
CustomMapping(
383+
a=1,
384+
b={"c": True},
385+
c=NestedMapping(d=3, e="f"),
386+
d=(NestedMapping(d=4, e="x"), NestedMapping(d=3, e="y")),
387+
e=[NestedMapping(d=4, e="z"), NestedMapping(d=5, e="w")],
388+
),
389+
"e.where(d=5)",
390+
[NestedMapping(d=5, e="w")],
391+
)
392+
],
393+
)
394+
def mappings_test(resource, path, expected):
395+
assert evaluate(resource, path) == expected

0 commit comments

Comments
 (0)