11import logging
22import os
33import re
4- import sys
54from collections import defaultdict
65from typing import Any , Callable , Union
76
87
98def create_logger (name : str = None , level : Union [int , str ] = logging .INFO ):
109 """Get or create a logger used for local debug."""
10+ logger = logging .getLogger (name )
11+
12+ # Avoid adding duplicate handlers
13+ if logger .handlers :
14+ return logger
1115
1216 formater = logging .Formatter (f"%(asctime)s-%(levelname)s-[{ name } ] %(message)s" , datefmt = "[%Y-%m-%d %H:%M:%S]" )
1317
1418 handler = logging .StreamHandler ()
1519 handler .setLevel (level )
1620 handler .setFormatter (formater )
1721
18- logger = logging .getLogger (name )
1922 logger .setLevel (level )
2023 logger .addHandler (handler )
2124
@@ -60,24 +63,23 @@ class JSONPath:
6063 REP_SELECT_CONTENT = re .compile (r"^([\w.']+)(, ?[\w.']+)+$" )
6164 REP_FILTER_CONTENT = re .compile (r"@([.\[].*?)(?=<=|>=|==|!=|>|<| in| not| is|\s|\)|$)|len\(@([.\[].*?)\)" )
6265 REP_PATH_SEGMENT = re .compile (r"(?:\.|^)(?P<dot>\w+)|\[['\"](?P<quote>.*?)['\"]\]|\[(?P<int>\d+)\]" )
63-
64- # annotations
65- f : list
66- segments : list
67- lpath : int
68- subx = defaultdict (list )
69- result : list
70- result_type : str
71- eval_func : callable
66+ REP_WORD_KEY = re .compile (r"^\w+$" )
67+ REP_REGEX_PATTERN = re .compile (r"=~\s*/(.*?)/" )
7268
7369 def __init__ (self , expr : str ):
70+ # Initialize instance variables
71+ self .subx = defaultdict (list )
72+ self .segments = []
73+ self .lpath = 0
74+ self .result = []
75+ self .result_type = "VALUE"
76+ self .eval_func = eval
77+
7478 expr = self ._parse_expr (expr )
7579 self .segments = [s for s in expr .split (JSONPath .SEP ) if s ]
7680 self .lpath = len (self .segments )
7781 logger .debug (f"segments : { self .segments } " )
7882
79- self .caller_globals = sys ._getframe (1 ).f_globals
80-
8183 def parse (self , obj , result_type = "VALUE" , eval_func = eval ):
8284 if not isinstance (obj , (list , dict )):
8385 raise TypeError ("obj must be a list or a dict." )
@@ -87,6 +89,7 @@ def parse(self, obj, result_type="VALUE", eval_func=eval):
8789 self .result_type = result_type
8890 self .eval_func = eval_func
8991
92+ # Reset state for each parse call
9093 self .result = []
9194 self ._trace (obj , 0 , "$" )
9295
@@ -172,13 +175,13 @@ def _traverse(f, obj, i: int, path: str, *args):
172175 f (v , i , f"{ path } [{ idx } ]" , * args )
173176 elif isinstance (obj , dict ):
174177 for k , v in obj .items ():
175- if re . match (r"^\w+$" , k ):
178+ if JSONPath . REP_WORD_KEY . match (k ):
176179 f (v , i , f"{ path } .{ k } " , * args )
177180 else :
178181 f (v , i , f"{ path } ['{ k } ']" , * args )
179182
180183 @staticmethod
181- def _getattr (obj : dict , path : str , * , convert_number_str = False ):
184+ def _getattr (obj : Any , path : str , * , convert_number_str = False ):
182185 r = obj
183186 for k in path .split ("." ):
184187 if isinstance (r , dict ):
@@ -268,7 +271,7 @@ def _trace(self, obj, i: int, path):
268271 step_key = step [1 :- 1 ]
269272
270273 if isinstance (obj , dict ) and step_key in obj :
271- if re . match (r"^\w+$" , step_key ):
274+ if JSONPath . REP_WORD_KEY . match (step_key ):
272275 self ._trace (obj [step_key ], i + 1 , f"{ path } .{ step_key } " )
273276 else :
274277 self ._trace (obj [step_key ], i + 1 , f"{ path } ['{ step_key } ']" )
@@ -285,8 +288,9 @@ def _trace(self, obj, i: int, path):
285288 # select
286289 if isinstance (obj , dict ) and JSONPath .REP_SELECT_CONTENT .fullmatch (step ):
287290 for k in step .split ("," ):
291+ k = k .strip () # Remove whitespace
288292 if k in obj :
289- if re . match (r"^\w+$" , k ):
293+ if JSONPath . REP_WORD_KEY . match (k ):
290294 self ._trace (obj [k ], i + 1 , f"{ path } .{ k } " )
291295 else :
292296 self ._trace (obj [k ], i + 1 , f"{ path } ['{ k } ']" )
@@ -298,7 +302,7 @@ def _trace(self, obj, i: int, path):
298302 step = JSONPath .REP_FILTER_CONTENT .sub (self ._gen_obj , step )
299303
300304 if "=~" in step :
301- step = re . sub (r"=~\s*/(.*?)/" , r"@ RegexPattern(r'\1')" , step )
305+ step = JSONPath . REP_REGEX_PATTERN . sub (r"@ RegexPattern(r'\1')" , step )
302306
303307 if isinstance (obj , dict ):
304308 self ._filter (obj , i + 1 , path , step )
@@ -316,7 +320,7 @@ def _trace(self, obj, i: int, path):
316320 obj = list (obj .items ())
317321 self ._sorter (obj , step [2 :- 1 ])
318322 for k , v in obj :
319- if re . match (r"^\w+$" , k ):
323+ if JSONPath . REP_WORD_KEY . match (k ):
320324 self ._trace (v , i + 1 , f"{ path } .{ k } " )
321325 else :
322326 self ._trace (v , i + 1 , f"{ path } ['{ k } ']" )
@@ -329,6 +333,7 @@ def _trace(self, obj, i: int, path):
329333 if isinstance (obj , dict ):
330334 obj_ = {}
331335 for k in step [1 :- 1 ].split ("," ):
336+ k = k .strip () # Remove whitespace
332337 v = self ._getattr (obj , k )
333338 if v is not JSONPath ._MISSING :
334339 obj_ [k ] = v
@@ -339,15 +344,25 @@ def _trace(self, obj, i: int, path):
339344 return
340345
341346 def update (self , obj : Union [list , dict ], value_or_func : Union [Any , Callable [[Any ], Any ]]) -> Any :
347+ """Update values in JSON object using JSONPath expression.
348+
349+ Args:
350+ obj: JSON object (dict or list) to update
351+ value_or_func: Static value or callable that transforms the current value
352+
353+ Returns:
354+ Updated object (modified in-place for nested paths, returns new value for root)
355+ """
342356 paths = self .parse (obj , result_type = "PATH" )
357+ is_func = callable (value_or_func )
358+
359+ # Handle root object update specially
360+ if len (paths ) == 1 and paths [0 ] == "$" :
361+ return value_or_func (obj ) if is_func else value_or_func
362+
343363 for path in paths :
344364 matches = list (JSONPath .REP_PATH_SEGMENT .finditer (path ))
345365 if not matches :
346- # Root object
347- if isinstance (value_or_func , Callable ):
348- obj = value_or_func (obj )
349- else :
350- obj = value_or_func
351366 continue
352367
353368 target = obj
@@ -371,10 +386,7 @@ def update(self, obj: Union[list, dict], value_or_func: Union[Any, Callable[[Any
371386 elif group ["int" ]:
372387 key = int (group ["int" ])
373388
374- if isinstance (value_or_func , Callable ):
375- target [key ] = value_or_func (target [key ])
376- else :
377- target [key ] = value_or_func
389+ target [key ] = value_or_func (target [key ]) if is_func else value_or_func
378390
379391 return obj
380392
@@ -393,12 +405,24 @@ def compile(expr):
393405 return JSONPath (expr )
394406
395407
396- # global cache
408+ # global cache with size limit to prevent memory leaks
397409_jsonpath_cache = {}
410+ _CACHE_MAX_SIZE = 128
398411
399412
400413def search (expr , data ):
401- global _jsonpath_cache
414+ """Search JSON data using JSONPath expression with instance caching.
415+
416+ Args:
417+ expr: JSONPath expression string
418+ data: JSON data (dict or list)
419+
420+ Returns:
421+ List of matched values
422+ """
402423 if expr not in _jsonpath_cache :
424+ # Simple LRU: clear cache when it grows too large
425+ if len (_jsonpath_cache ) >= _CACHE_MAX_SIZE :
426+ _jsonpath_cache .clear ()
403427 _jsonpath_cache [expr ] = JSONPath (expr )
404428 return _jsonpath_cache [expr ].parse (data )
0 commit comments