Skip to content

Commit 9666eb5

Browse files
authored
Merge pull request #61 from brianpos/BP-2025-07-BugFixesAndEnhancements
Bug fixes and enhancements
2 parents cd5250a + c1bc7e2 commit 9666eb5

8 files changed

Lines changed: 104 additions & 17 deletions

File tree

.gitattributes

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Set default behavior to automatically normalize line endings
2+
* text=auto
3+
4+
# Force specific files to have Unix line endings (LF)
5+
*.py text eol=lf
6+
*.md text eol=lf
7+
*.yml text eol=lf
8+
*.yaml text eol=lf
9+
*.toml text eol=lf
10+
*.ini text eol=lf
11+
*.txt text eol=lf
12+
*.json text eol=lf
13+
14+
# Ensure these files are always treated as text and use LF
15+
Dockerfile text eol=lf
16+
*.dockerfile text eol=lf
17+
18+
# Declare files that will always have CRLF line endings on checkout
19+
*.bat text eol=crlf
20+
21+
# Denote all files that are truly binary and should not be modified
22+
*.png binary
23+
*.jpg binary
24+
*.jpeg binary
25+
*.gif binary
26+
*.ico binary
27+
*.pdf binary
28+
*.zip binary
29+
*.tar.gz binary
30+
*.whl binary

fhirpathpy/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,31 @@ def apply_parsed_path(resource, parsedPath, context=None, model=None, options=No
3535
(options or {}).get("userInvocationTable", {})
3636
),
3737
}
38+
39+
# Add trace callback if provided in options
40+
if options and "traceFn" in options:
41+
ctx["traceFn"] = options["traceFn"]
42+
3843
node = do_eval(ctx, dataRoot, parsedPath["children"][0])
3944

4045
# Resolve any internal "ResourceNode" instances. Continue to let FP_Type
4146
# subclasses through.
4247

48+
if options and options.get("returnRawData", False):
49+
if isinstance(node, list):
50+
res = []
51+
# Filter out intenal representation of primitive extensions
52+
# even in this raw data mode (as they are not a part of the output)
53+
for item in node:
54+
if isinstance(item, ResourceNode):
55+
if isinstance(item.data, dict):
56+
keys = list(item.data.keys())
57+
if keys == ["extension"]:
58+
continue
59+
res.append(item)
60+
return res
61+
return node
62+
4363
def visit(node):
4464
data = get_data(node)
4565

fhirpathpy/engine/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ def doInvoke(ctx, fn_name, data, raw_params):
8383
params = []
8484
argTypes = invocation["arity"][paramsNumber]
8585

86+
thisValue = ctx["$this"] if "$this" in ctx else ctx["dataRoot"]
8687
for i in range(0, paramsNumber):
8788
tp = argTypes[i]
8889
pr = raw_params[i]
89-
params.append(make_param(ctx, data, tp, pr))
90+
params.append(make_param(ctx, thisValue, tp, pr))
9091

9192
params.insert(0, data)
9293
params.insert(0, ctx)

fhirpathpy/engine/evaluators/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ def create_reduce_member_invocation(model, key):
174174
def func(acc, res):
175175
res = nodes.ResourceNode.create_node(res)
176176
childPath = f"{res.path}.{key}" if res.path else f"_.{key}"
177+
fullPath = f"{res.propName}.{key}" if res.propName else childPath # The full path to the node (weill evenutally be) e.g. Patient.name[0].given
178+
fullPath = fullPath.replace("_", "")
177179

178180
actualTypes = None
179181
toAdd = None
@@ -212,16 +214,16 @@ def func(acc, res):
212214

213215
if util.is_some(toAdd):
214216
if isinstance(toAdd, list):
215-
mapped = [nodes.ResourceNode.create_node(x, childPath) for x in toAdd]
217+
mapped = [nodes.ResourceNode.create_node(x, childPath, propName=f"{fullPath}[{i}]", index=i) for i, x in enumerate(toAdd)]
216218
acc = acc + mapped
217219
else:
218-
acc.append(nodes.ResourceNode.create_node(toAdd, childPath))
220+
acc.append(nodes.ResourceNode.create_node(toAdd, childPath, propName=fullPath))
219221
if util.is_some(toAdd_):
220222
if isinstance(toAdd_, list):
221-
mapped = [nodes.ResourceNode.create_node(x, childPath) for x in toAdd_]
223+
mapped = [nodes.ResourceNode.create_node(x, childPath, propName=f"{fullPath}[{i}]", index=i) for i, x in enumerate(toAdd_)]
222224
acc = acc + mapped
223225
else:
224-
acc.append(nodes.ResourceNode.create_node(toAdd_, childPath))
226+
acc.append(nodes.ResourceNode.create_node(toAdd_, childPath, propName=fullPath))
225227
return acc
226228

227229
return func

fhirpathpy/engine/invocations/misc.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ def iif_macro(ctx, data, cond, ok, fail=None):
2121

2222

2323
def trace_fn(ctx, x, label=""):
24-
print("TRACE:[" + label + "]", str(x))
24+
# Check if a custom trace callback is provided in the context
25+
if "traceFn" in ctx and callable(ctx["traceFn"]):
26+
ctx["traceFn"](label, x)
27+
else:
28+
# Fall back to console output if no callback is provided
29+
print("TRACE:[" + label + "]", str(x))
2530
return x
2631

2732

fhirpathpy/engine/invocations/navigation.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
create_node = nodes.ResourceNode.create_node
77

88

9-
def create_reduce_children(ctx):
9+
def create_reduce_children(ctx, exclude_primitive_extensions):
1010
model = ctx["model"]
1111

1212
def func(acc, res):
@@ -21,35 +21,61 @@ def func(acc, res):
2121
value = data[prop]
2222
childPath = ""
2323

24+
# extensions shouldn't filter through here, yet they should for descendants?
25+
# unless this item is the node that is being processed (primitive extension)
26+
# though if you filter it, descendants will not work too
27+
if prop.startswith("_") and exclude_primitive_extensions:
28+
continue
29+
2430
if res.path is not None:
2531
childPath = res.path + "." + prop
2632

33+
fullPath = f"{res.propName}.{prop}" if res.propName else childPath # The full path to the node (weill evenutally be) e.g. Patient.name[0].given
34+
fullPath = fullPath.replace("_", "")
35+
36+
if prop == "extension":
37+
childPath = "Extension"
38+
2739
if (
2840
isinstance(model, dict)
2941
and "pathsDefinedElsewhere" in model
3042
and childPath in model["pathsDefinedElsewhere"]
3143
):
3244
childPath = model["pathsDefinedElsewhere"][childPath]
3345

46+
childPath = (
47+
model["path2Type"].get(childPath, childPath)
48+
if isinstance(model, dict) and "path2Type" in model
49+
else childPath
50+
)
51+
52+
# If the prop tolower ends with the type tolower
53+
if prop.lower().endswith(childPath.lower()) and len(prop) > len(childPath):
54+
# Check if the path is actually in the choice types
55+
altPropName = res.path + "." + prop[:-len(childPath)]
56+
actualTypes = model["choiceTypePaths"].get(altPropName, [])
57+
if len(actualTypes) > 0:
58+
# If it is, we can use it
59+
fullPath = f"{res.propName}.{prop[:-len(childPath)]}"
60+
3461
if isinstance(value, list):
35-
mapped = [create_node(n, childPath) for n in value]
62+
mapped = [create_node(n, childPath, propName=f"{fullPath}[{i}]", index=i) for i, n in enumerate(value)]
3663
acc = acc + mapped
3764
else:
38-
acc.append(create_node(value, childPath))
65+
acc.append(create_node(value, childPath, propName=fullPath))
3966
return acc
4067

4168
return func
4269

4370

4471
def children(ctx, coll):
45-
return reduce(create_reduce_children(ctx), coll, [])
72+
return reduce(create_reduce_children(ctx, True), coll, [])
4673

4774

4875
def descendants(ctx, coll):
4976
res = []
50-
ch = children(ctx, coll)
77+
ch = reduce(create_reduce_children(ctx, False), coll, [])
5178
while len(ch) > 0:
5279
res = res + ch
53-
ch = children(ctx, ch)
54-
80+
ch = reduce(create_reduce_children(ctx, False), ch, [])
5581
return res

fhirpathpy/engine/nodes.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import json
99
import re
1010
import time
11+
from typing import Optional
1112

1213

1314
timeRE = (
@@ -821,7 +822,7 @@ class ResourceNode:
821822
* @param _data additional data stored in a property named with "_" prepended.
822823
"""
823824

824-
def __init__(self, data, path, _data=None):
825+
def __init__(self, data, path, _data=None, propName=None, index=None):
825826
"""
826827
If data is a resource (maybe a contained resource) reset the path
827828
information to the resource type.
@@ -832,6 +833,8 @@ def __init__(self, data, path, _data=None):
832833
self.path = path
833834
self.data = data
834835
self._data = _data
836+
self.propName: Optional[str] = propName
837+
self.index: Optional[int] = index
835838

836839
def __eq__(self, value):
837840
if isinstance(value, ResourceNode):
@@ -864,10 +867,10 @@ def toJSON(self):
864867
return json.dumps(self.data)
865868

866869
@staticmethod
867-
def create_node(data, path=None, _data=None):
870+
def create_node(data, path=None, _data=None, propName=None, index=None):
868871
if isinstance(data, ResourceNode):
869872
return data
870-
return ResourceNode(data, path, _data)
873+
return ResourceNode(data, path, _data, propName, index)
871874

872875
def convert_data(self):
873876
data = self.data

tests/test_evaluators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ def existence_functions_test(resource, path, expected):
193193
("resource", "path", "expected"),
194194
[
195195
({}, "iif(true, 'a', 'b')", ["a"]),
196-
({"a": {"b": [1, 2, 3]}}, "a.b.trace()", [1, 2, 3]),
196+
({"a": {"b": [1, 2, 3]}}, "a.b.trace('t')", [1, 2, 3]),
197197
({"a": True}, "a.toInteger()", [1]),
198198
({"a": False}, "a.toInteger()", [0]),
199199
({"a": True}, "a.toDecimal()", [1.0]),

0 commit comments

Comments
 (0)