@@ -152,7 +152,7 @@ def __init__(
152152
153153 self ._pattern_value : dict [int , _RegexPattern ] = {}
154154 self ._data = None
155- self ._visited_paths = set ()
155+ self ._visited_paths = {}
156156 return
157157
158158 def fill (
@@ -162,17 +162,17 @@ def fill(
162162 current_path : str = "" ,
163163 ):
164164 self ._data = data
165- self ._visited_paths = set ()
166- path = ( f"$.{ current_path } " if self ._add_prefix else current_path ) if current_path else "$"
165+ self ._visited_paths = {}
166+ path = _jsonpath . parse (( f"$.{ current_path } " if self ._add_prefix else current_path ) if current_path else "$" )
167167 return self ._recursive_subst (
168168 templ = template or data ,
169169 current_path = path ,
170170 relative_path_anchor = path ,
171171 level = 0 ,
172- current_chain = [ path ] ,
172+ current_chain = ( path ,) ,
173173 )
174174
175- def _recursive_subst (self , templ , current_path : str , relative_path_anchor : str , level : int , current_chain : list [str ]):
175+ def _recursive_subst (self , templ , current_path : str , relative_path_anchor : str , level : int , current_chain : tuple [str , ... ]):
176176
177177 def get_code_value (match : _re .Match | str ):
178178
@@ -207,7 +207,7 @@ def getter_function(path: str, default: Any = None, search: bool = False):
207207 path_invalid = current_path ,
208208 exception = e ,
209209 )
210- self ._visited_paths . add ( self . _normalize_path ( current_path ) )
210+ self ._visited_paths [ current_path ] = ( output , True )
211211 return output
212212
213213 def get_address_value (match : _re .Match | str , return_all_matches : bool = False , from_code : bool = False ):
@@ -219,30 +219,30 @@ def get_address_value(match: _re.Match | str, return_all_matches: bool = False,
219219 path_expr = _jsonpath .parse (path )
220220 except _jsonpath_exceptions .JSONPathError :
221221 raise_error (
222- path_invalid = path ,
222+ path_invalid = path_expr ,
223223 description_template = "JSONPath expression {path_invalid} is invalid." ,
224224 )
225225 if num_periods :
226226 if relative_path_anchor != current_path :
227- path_fields = self ._extract_fields (_jsonpath . parse ( current_path ) )
227+ path_fields = self ._extract_fields (current_path )
228228 has_template_key = any (field in self ._template_keys for field in path_fields )
229229 anchor_path = relative_path_anchor if has_template_key else current_path
230230 else :
231231 anchor_path = current_path
232- root_path_expr = _jsonpath . parse ( anchor_path )
232+ root_path_expr = anchor_path
233233 for period in range (num_periods ):
234234 if isinstance (root_path_expr , _jsonpath .Root ):
235235 raise_error (
236- path_invalid = path ,
236+ path_invalid = path_expr ,
237237 description_template = (
238238 "Relative path {path_invalid} is invalid; "
239239 f"reached root but still { num_periods - period } levels remaining."
240240 ),
241241 )
242242 root_path_expr = root_path_expr .left
243243 path_expr = self ._concat_json_paths (root_path_expr , path_expr )
244- value , matched = get_value (path_expr , return_all_matches , from_code )
245- self ._visited_paths . add ( self . _normalize_path ( current_path ) )
244+ value , matched = self . _visited_paths . get ( path_expr ) or get_value (path_expr , return_all_matches , from_code )
245+ self ._visited_paths [ path_expr ] = ( value , matched )
246246 if from_code :
247247 return value , matched
248248 if matched :
@@ -260,7 +260,7 @@ def get_value(jsonpath, return_all_matches: bool, from_code: bool) -> tuple[Any,
260260 return [], True
261261 if self ._raise_no_match :
262262 raise_error (
263- path_invalid = str ( jsonpath ) ,
263+ path_invalid = jsonpath ,
264264 description_template = "JSONPath expression {path_invalid} did not match any data." ,
265265 )
266266 return None , False
@@ -274,10 +274,10 @@ def get_value(jsonpath, return_all_matches: bool, from_code: bool) -> tuple[Any,
274274 _rel_path_anchor = relative_path_anchor
275275 return self ._recursive_subst (
276276 output ,
277- current_path = str ( jsonpath ) ,
277+ current_path = jsonpath ,
278278 relative_path_anchor = _rel_path_anchor ,
279279 level = 0 ,
280- current_chain = current_chain + [ str (jsonpath )] ,
280+ current_chain = current_chain + (jsonpath ,) ,
281281 ), True
282282
283283 def _rec_match (expr ) -> list :
@@ -291,10 +291,10 @@ def _rec_match(expr) -> list:
291291 for left_match in left_matches :
292292 left_match_filled = self ._recursive_subst (
293293 templ = left_match .value ,
294- current_path = str ( expr .left ) ,
295- relative_path_anchor = str ( expr .left ) ,
294+ current_path = expr .left ,
295+ relative_path_anchor = expr .left ,
296296 level = 0 ,
297- current_chain = current_chain + [ str (expr .left )] ,
297+ current_chain = current_chain + (expr .left ,) ,
298298 ) if isinstance (left_match .value , str ) else left_match .value
299299 right_matches = expr .right .find (left_match_filled )
300300 whole_matches .extend (right_matches )
@@ -341,9 +341,10 @@ def raise_error(path_invalid: str, description_template: str, exception: Excepti
341341 template_end = self ._marker_end_value ,
342342 ) from exception
343343
344+ if current_path in self ._visited_paths :
345+ return self ._visited_paths [current_path ][0 ]
346+
344347 self ._check_endless_loop (templ , current_chain )
345- # if self._normalize_path(current_path) in self._visited_paths:
346- # return templ
347348
348349 if isinstance (templ , str ):
349350 # Handle value blocks
@@ -391,13 +392,13 @@ def raise_error(path_invalid: str, description_template: str, exception: Excepti
391392 if isinstance (templ , list ):
392393 out = []
393394 for idx , elem in enumerate (templ ):
394- new_path = f" { current_path } [ { idx } ]"
395+ new_path = _jsonpath . Child ( current_path , _jsonpath . Index ( idx ))
395396 elem_filled = self ._recursive_subst (
396397 templ = elem ,
397398 current_path = new_path ,
398399 relative_path_anchor = get_relative_path (new_path ),
399400 level = 0 ,
400- current_chain = current_chain + [ new_path ] ,
401+ current_chain = current_chain + ( new_path ,) ,
401402 )
402403 if isinstance (elem , str ) and self ._pattern_unpack .fullmatch (elem ):
403404 try :
@@ -409,7 +410,7 @@ def raise_error(path_invalid: str, description_template: str, exception: Excepti
409410 )
410411 else :
411412 out .append (elem_filled )
412- self ._visited_paths . add ( self . _normalize_path ( current_path ) )
413+ self ._visited_paths [ current_path ] = ( out , True )
413414 return out
414415
415416 if isinstance (templ , dict ):
@@ -428,29 +429,30 @@ def raise_error(path_invalid: str, description_template: str, exception: Excepti
428429 if key_filled in self ._template_keys :
429430 new_dict [key_filled ] = val
430431 continue
431- new_path = f" { current_path } .' { key_filled } '"
432+ new_path = _jsonpath . Child ( current_path , _jsonpath . Fields ( key_filled ))
432433 new_dict [key_filled ] = self ._recursive_subst (
433434 templ = val ,
434435 current_path = new_path ,
435436 relative_path_anchor = get_relative_path (new_path ),
436437 level = 0 ,
437- current_chain = current_chain + [ new_path ] ,
438+ current_chain = current_chain + ( new_path ,) ,
438439 )
439- self ._visited_paths . add ( self . _normalize_path ( current_path ) )
440+ self ._visited_paths [ current_path ] = ( new_dict , True )
440441 return new_dict
441442 return templ
442443
443- def _check_endless_loop (self ,templ , chain : list [str ]):
444- last_idx = len (chain ) - 1
444+ def _check_endless_loop (self ,templ , chain : tuple [str , ... ]):
445+ last_idx = len (chain ) - 1
445446 first_idx = chain .index (chain [- 1 ])
446447 if first_idx == last_idx :
447448 return
448- loop = chain [first_idx - 1 : - 1 ]
449- loop_str = "\n " .join ([f"- { path .replace ("'" , "" )} " for path in loop ])
449+ loop = [chain [- 2 ], * chain [first_idx : - 2 ]]
450+ loop_str = "\n " .join ([f"- { path } " for path in loop ])
451+ history_str = "\n " .join ([f"- { path } " for path in chain ])
450452 raise _exception .update .PySerialsUpdateTemplatedDataError (
451- description_template = f"Path {{path_invalid}} starts a loop:\n { loop_str } " ,
452- path_invalid = loop [ 0 ],
453- path = chain [- 1 ],
453+ description_template = f"Path {{path_invalid}} starts a loop:\n { loop_str } \n History: \n { history_str } " ,
454+ path_invalid = chain [ - 2 ],
455+ path = chain [0 ],
454456 data = templ ,
455457 data_full = self ._data ,
456458 data_source = self ._data ,
0 commit comments