diff --git a/jsonpath_ng/jsonpath.py b/jsonpath_ng/jsonpath.py index db17a18..550b52c 100644 --- a/jsonpath_ng/jsonpath.py +++ b/jsonpath_ng/jsonpath.py @@ -1,3 +1,5 @@ +from __future__ import annotations +from typing import List, Optional import logging from itertools import * # noqa from jsonpath_ng.lexer import JsonPathLexer @@ -20,7 +22,7 @@ class JSONPath: JSONPath semantics. """ - def find(self, data): + def find(self, data) -> List[DatumInContext]: """ All `JSONPath` types support `find()`, which returns an iterable of `DatumInContext`s. They keep track of the path followed to the current location, so if the calling code @@ -99,11 +101,21 @@ def wrap(cls, data): else: return cls(data) - def __init__(self, value, path=None, context=None): - self.value = value + def __init__(self, value, path: Optional[JSONPath]=None, context: Optional[DatumInContext]=None): + self.__value__ = value self.path = path or This() self.context = None if context is None else DatumInContext.wrap(context) + @property + def value(self): + return self.__value__ + + @value.setter + def value(self, value): + if self.context is not None and self.context.value is not None: + self.path.update(self.context.value, value) + self.__value__ = value + def in_context(self, context, path): context = DatumInContext.wrap(context) @@ -113,7 +125,7 @@ def in_context(self, context, path): return DatumInContext(value=self.value, path=path, context=context) @property - def full_path(self): + def full_path(self) -> JSONPath: return self.path if self.context is None else self.context.full_path.child(self.path) @property @@ -193,7 +205,7 @@ class Root(JSONPath): The root is the topmost datum without any context attached. """ - def find(self, data): + def find(self, data) -> List[DatumInContext]: if not isinstance(data, DatumInContext): return [DatumInContext(data, path=Root(), context=None)] else: @@ -692,7 +704,7 @@ def _find_base(self, datum, create): for index in self.indices: # invalid indices do not crash, return [] instead if datum.value and len(datum.value) > index: - rv += [DatumInContext(datum.value[index], path=self, context=datum)] + rv += [DatumInContext(datum.value[index], path=Index(index), context=datum)] return rv def update(self, data, val): diff --git a/jsonpath_ng/lexer.py b/jsonpath_ng/lexer.py index bc86488..7d84ed6 100644 --- a/jsonpath_ng/lexer.py +++ b/jsonpath_ng/lexer.py @@ -64,7 +64,9 @@ def tokenize(self, string): t_ignore = ' \t' def t_ID(self, t): - r'[a-zA-Z_@][a-zA-Z0-9_@\-]*' + # CJK: [\u4E00-\u9FA5] + # EMOJI: [\U0001F600-\U0001F64F] + r'([a-zA-Z_@]|[\u4E00-\u9FA5]|[\U0001F600-\U0001F64F])([a-zA-Z0-9_@\-]|[\u4E00-\u9FA5]|[\U0001F600-\U0001F64F])*' t.type = self.reserved_words.get(t.value, 'ID') return t diff --git a/jsonpath_ng/parser.py b/jsonpath_ng/parser.py index d3d6c63..d049a8f 100644 --- a/jsonpath_ng/parser.py +++ b/jsonpath_ng/parser.py @@ -53,7 +53,7 @@ def __init__(self, debug=False, lexer_class=None): start = start_symbol, errorlog = logger) - def parse(self, string, lexer = None): + def parse(self, string, lexer = None) -> JSONPath: lexer = lexer or self.lexer_class() return self.parse_token_stream(lexer.tokenize(string)) diff --git a/requirements-dev.txt b/requirements-dev.txt index ccdfca6..fb1680c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ tox flake8 +pytest diff --git a/tests/test_jsonpath.py b/tests/test_jsonpath.py index f1f27de..abd105e 100644 --- a/tests/test_jsonpath.py +++ b/tests/test_jsonpath.py @@ -1,11 +1,12 @@ import copy import pytest - +from typing import Callable from jsonpath_ng.ext.parser import parse as ext_parse from jsonpath_ng.jsonpath import DatumInContext, Fields, Root, This from jsonpath_ng.lexer import JsonPathLexerError from jsonpath_ng.parser import parse as base_parse +from jsonpath_ng import JSONPath from .helpers import assert_full_path_equality, assert_value_equality @@ -166,12 +167,26 @@ def test_datumincontext_in_context_nested(): update_test_cases, ) @parsers -def test_update(parse, expression, data, update_value, expected_value): +def test_update(parse: Callable[[str], JSONPath], expression: str, data, update_value, expected_value): data_copy = copy.deepcopy(data) update_value_copy = copy.deepcopy(update_value) result = parse(expression).update(data_copy, update_value_copy) assert result == expected_value + # inplace update testing + data_copy2 = copy.deepcopy(data) + update_value_copy2 = copy.deepcopy(update_value) + datums = parse(expression).find(data_copy2) + batch_update = isinstance(update_value, list) and len(datums) == len(update_value) + for i, datum in enumerate(datums): + if batch_update: + datum.value = update_value_copy2[i] + else: + datum.value = update_value_copy2 + if isinstance(datum.full_path, (Root, This)): # when the type of `data` is str, int, float etc. + data_copy2 = datum.value + assert data_copy2 == expected_value + find_test_cases = ( # diff --git a/tests/test_parser.py b/tests/test_parser.py index df22d0b..20d140b 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -10,6 +10,8 @@ # Atomic # ------ # + ("😀", Fields("😀")), + ("你好", Fields("你好")), ("foo", Fields("foo")), ("*", Fields("*")), ("1", Fields("1")),