python-jsonpath 1.2.1__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/PKG-INFO +5 -5
  2. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/__about__.py +1 -1
  3. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/__init__.py +2 -0
  4. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/filter.py +36 -3
  5. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/lex.py +12 -12
  6. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/match.py +5 -0
  7. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/selectors.py +22 -23
  8. python_jsonpath-1.3.0/jsonpath/serialize.py +13 -0
  9. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/pyproject.toml +3 -4
  10. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/.gitignore +0 -0
  11. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/LICENSE.txt +0 -0
  12. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/README.md +0 -0
  13. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/__main__.py +0 -0
  14. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/_data.py +0 -0
  15. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/cli.py +0 -0
  16. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/env.py +0 -0
  17. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/exceptions.py +0 -0
  18. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/fluent_api.py +0 -0
  19. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/__init__.py +0 -0
  20. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/arguments.py +0 -0
  21. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/count.py +0 -0
  22. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/filter_function.py +0 -0
  23. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/is_instance.py +0 -0
  24. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/keys.py +0 -0
  25. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/length.py +0 -0
  26. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/match.py +0 -0
  27. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/search.py +0 -0
  28. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/typeof.py +0 -0
  29. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/function_extensions/value.py +0 -0
  30. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/parse.py +0 -0
  31. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/patch.py +0 -0
  32. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/path.py +0 -0
  33. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/pointer.py +0 -0
  34. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/py.typed +0 -0
  35. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/stream.py +0 -0
  36. {python_jsonpath-1.2.1 → python_jsonpath-1.3.0}/jsonpath/token.py +0 -0
@@ -1,17 +1,17 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: python-jsonpath
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: JSONPath, JSON Pointer and JSON Patch for Python.
5
5
  Project-URL: Documentation, https://jg-rp.github.io/python-jsonpath/
6
6
  Project-URL: Issues, https://github.com/jg-rp/python-jsonpath/issues
7
7
  Project-URL: Source, https://github.com/jg-rp/python-jsonpath
8
8
  Author-email: James Prior <jamesgr.prior@gmail.com>
9
- License: MIT
9
+ License-Expression: MIT
10
+ License-File: LICENSE.txt
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
12
  Classifier: Intended Audience :: Developers
12
13
  Classifier: License :: OSI Approved :: MIT License
13
14
  Classifier: Programming Language :: Python
14
- Classifier: Programming Language :: Python :: 3.7
15
15
  Classifier: Programming Language :: Python :: 3.8
16
16
  Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
22
22
  Classifier: Programming Language :: Python :: Implementation :: PyPy
23
- Requires-Python: >=3.7
23
+ Requires-Python: >=3.8
24
24
  Description-Content-Type: text/markdown
25
25
 
26
26
  <h1 align="center">Python JSONPath</h1>
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2023-present James Prior <jamesgr.prior@gmail.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "1.2.1"
4
+ __version__ = "1.3.0"
@@ -24,6 +24,7 @@ from .fluent_api import Projection
24
24
  from .fluent_api import Query
25
25
  from .lex import Lexer
26
26
  from .match import JSONPathMatch
27
+ from .match import NodeList
27
28
  from .parse import Parser
28
29
  from .patch import JSONPatch
29
30
  from .path import CompoundJSONPath
@@ -58,6 +59,7 @@ __all__ = (
58
59
  "JSONPointerResolutionError",
59
60
  "JSONPointerTypeError",
60
61
  "Lexer",
62
+ "NodeList",
61
63
  "match",
62
64
  "Parser",
63
65
  "Projection",
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import copy
6
- import json
7
6
  import re
8
7
  from abc import ABC
9
8
  from abc import abstractmethod
@@ -25,6 +24,7 @@ from .function_extensions import FilterFunction
25
24
  from .match import NodeList
26
25
  from .selectors import Filter as FilterSelector
27
26
  from .selectors import ListSelector
27
+ from .serialize import canonical_string
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  from .path import JSONPath
@@ -208,7 +208,7 @@ class StringLiteral(Literal[str]):
208
208
  __slots__ = ()
209
209
 
210
210
  def __str__(self) -> str:
211
- return json.dumps(self.value)
211
+ return canonical_string(self.value)
212
212
 
213
213
 
214
214
  class IntegerLiteral(Literal[int]):
@@ -375,6 +375,12 @@ class InfixExpression(FilterExpression):
375
375
  self.right = children[1]
376
376
 
377
377
 
378
+ PRECEDENCE_LOWEST = 1
379
+ PRECEDENCE_LOGICAL_OR = 3
380
+ PRECEDENCE_LOGICAL_AND = 4
381
+ PRECEDENCE_PREFIX = 7
382
+
383
+
378
384
  class BooleanExpression(FilterExpression):
379
385
  """An expression that always evaluates to `True` or `False`."""
380
386
 
@@ -408,13 +414,40 @@ class BooleanExpression(FilterExpression):
408
414
  )
409
415
 
410
416
  def __str__(self) -> str:
411
- return str(self.expression)
417
+ return self._canonical_string(self.expression, PRECEDENCE_LOWEST)
412
418
 
413
419
  def __eq__(self, other: object) -> bool:
414
420
  return (
415
421
  isinstance(other, BooleanExpression) and self.expression == other.expression
416
422
  )
417
423
 
424
+ def _canonical_string(
425
+ self, expression: FilterExpression, parent_precedence: int
426
+ ) -> str:
427
+ if isinstance(expression, InfixExpression):
428
+ if expression.operator == "&&":
429
+ left = self._canonical_string(expression.left, PRECEDENCE_LOGICAL_AND)
430
+ right = self._canonical_string(expression.right, PRECEDENCE_LOGICAL_AND)
431
+ expr = f"{left} && {right}"
432
+ return (
433
+ f"({expr})" if parent_precedence >= PRECEDENCE_LOGICAL_AND else expr
434
+ )
435
+
436
+ if expression.operator == "||":
437
+ left = self._canonical_string(expression.left, PRECEDENCE_LOGICAL_OR)
438
+ right = self._canonical_string(expression.right, PRECEDENCE_LOGICAL_OR)
439
+ expr = f"{left} || {right}"
440
+ return (
441
+ f"({expr})" if parent_precedence >= PRECEDENCE_LOGICAL_OR else expr
442
+ )
443
+
444
+ if isinstance(expression, PrefixExpression):
445
+ operand = self._canonical_string(expression.right, PRECEDENCE_PREFIX)
446
+ expr = f"!{operand}"
447
+ return f"({expr})" if parent_precedence > PRECEDENCE_PREFIX else expr
448
+
449
+ return str(expression)
450
+
418
451
  def evaluate(self, context: FilterContext) -> bool:
419
452
  return context.env.is_truthy(self.expression.evaluate(context))
420
453
 
@@ -88,13 +88,13 @@ class Lexer:
88
88
  key_pattern = r"[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*"
89
89
 
90
90
  # `not` or !
91
- logical_not_pattern = r"(?:not|!)"
91
+ logical_not_pattern = r"(?:not\b)|!"
92
92
 
93
93
  # && or `and`
94
- logical_and_pattern = r"(?:&&|and)"
94
+ logical_and_pattern = r"&&|(?:and\b)"
95
95
 
96
96
  # || or `or`
97
- logical_or_pattern = r"(?:\|\||or)"
97
+ logical_or_pattern = r"\|\||(?:or\b)"
98
98
 
99
99
  def __init__(self, *, env: JSONPathEnvironment) -> None:
100
100
  self.env = env
@@ -153,15 +153,15 @@ class Lexer:
153
153
  ],
154
154
  (TOKEN_WILD, r"\*"),
155
155
  (TOKEN_FILTER, r"\?"),
156
- (TOKEN_IN, r"in"),
157
- (TOKEN_TRUE, r"[Tt]rue"),
158
- (TOKEN_FALSE, r"[Ff]alse"),
159
- (TOKEN_NIL, r"[Nn]il"),
160
- (TOKEN_NULL, r"[Nn]ull"),
161
- (TOKEN_NONE, r"[Nn]one"),
162
- (TOKEN_CONTAINS, r"contains"),
163
- (TOKEN_UNDEFINED, r"undefined"),
164
- (TOKEN_MISSING, r"missing"),
156
+ (TOKEN_IN, r"in\b"),
157
+ (TOKEN_TRUE, r"[Tt]rue\b"),
158
+ (TOKEN_FALSE, r"[Ff]alse\b"),
159
+ (TOKEN_NIL, r"[Nn]il\b"),
160
+ (TOKEN_NULL, r"[Nn]ull\b"),
161
+ (TOKEN_NONE, r"[Nn]one\b"),
162
+ (TOKEN_CONTAINS, r"contains\b"),
163
+ (TOKEN_UNDEFINED, r"undefined\b"),
164
+ (TOKEN_MISSING, r"missing\b"),
165
165
  (TOKEN_LIST_START, r"\["),
166
166
  (TOKEN_RBRACKET, r"]"),
167
167
  (TOKEN_COMMA, r","),
@@ -1,4 +1,5 @@
1
1
  """The JSONPath match object, as returned from `JSONPath.finditer()`."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from typing import Any
@@ -104,6 +105,10 @@ class NodeList(List[JSONPathMatch]):
104
105
  return self[0].obj
105
106
  return [match.obj for match in self]
106
107
 
108
+ def paths(self) -> List[str]:
109
+ """Return a normalized path for each node in this node list."""
110
+ return [match.path for match in self]
111
+
107
112
  def empty(self) -> bool:
108
113
  """Return `True` if this node list is empty."""
109
114
  return not bool(self)
@@ -1,4 +1,5 @@
1
1
  """JSONPath segments and selectors, as returned from `Parser.parse`."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from abc import ABC
@@ -17,6 +18,7 @@ from typing import Union
17
18
 
18
19
  from .exceptions import JSONPathIndexError
19
20
  from .exceptions import JSONPathTypeError
21
+ from .serialize import canonical_string
20
22
 
21
23
  if TYPE_CHECKING:
22
24
  from .env import JSONPathEnvironment
@@ -75,7 +77,11 @@ class PropertySelector(JSONPathSelector):
75
77
  self.shorthand = shorthand
76
78
 
77
79
  def __str__(self) -> str:
78
- return f"['{self.name}']" if self.shorthand else f"'{self.name}'"
80
+ return (
81
+ f"[{canonical_string(self.name)}]"
82
+ if self.shorthand
83
+ else f"{canonical_string(self.name)}"
84
+ )
79
85
 
80
86
  def __eq__(self, __value: object) -> bool:
81
87
  return (
@@ -98,7 +104,7 @@ class PropertySelector(JSONPathSelector):
98
104
  obj=self.env.getitem(match.obj, self.name),
99
105
  parent=match,
100
106
  parts=match.parts + (self.name,),
101
- path=match.path + f"['{self.name}']",
107
+ path=match.path + f"[{canonical_string(self.name)}]",
102
108
  root=match.root,
103
109
  )
104
110
  match.add_child(_match)
@@ -117,7 +123,7 @@ class PropertySelector(JSONPathSelector):
117
123
  obj=await self.env.getitem_async(match.obj, self.name),
118
124
  parent=match,
119
125
  parts=match.parts + (self.name,),
120
- path=match.path + f"['{self.name}']",
126
+ path=match.path + f"[{canonical_string(self.name)}]",
121
127
  root=match.root,
122
128
  )
123
129
  match.add_child(_match)
@@ -321,20 +327,15 @@ class SliceSelector(JSONPathSelector):
321
327
  ):
322
328
  raise JSONPathIndexError("index out of range", token=self.token)
323
329
 
324
- def _normalized_index(self, obj: Sequence[object], index: int) -> int:
325
- if index < 0 and len(obj) >= abs(index):
326
- return len(obj) + index
327
- return index
328
-
329
330
  def resolve(self, matches: Iterable[JSONPathMatch]) -> Iterable[JSONPathMatch]:
330
331
  for match in matches:
331
332
  if not isinstance(match.obj, Sequence) or self.slice.step == 0:
332
333
  continue
333
334
 
334
- idx = self.slice.start or 0
335
- step = self.slice.step or 1
336
- for obj in self.env.getitem(match.obj, self.slice):
337
- norm_index = self._normalized_index(match.obj, idx)
335
+ for norm_index, obj in zip( # noqa: B905
336
+ range(*self.slice.indices(len(match.obj))),
337
+ self.env.getitem(match.obj, self.slice),
338
+ ):
338
339
  _match = self.env.match_class(
339
340
  filter_context=match.filter_context(),
340
341
  obj=obj,
@@ -345,7 +346,6 @@ class SliceSelector(JSONPathSelector):
345
346
  )
346
347
  match.add_child(_match)
347
348
  yield _match
348
- idx += step
349
349
 
350
350
  async def resolve_async(
351
351
  self, matches: AsyncIterable[JSONPathMatch]
@@ -354,10 +354,10 @@ class SliceSelector(JSONPathSelector):
354
354
  if not isinstance(match.obj, Sequence) or self.slice.step == 0:
355
355
  continue
356
356
 
357
- idx = self.slice.start or 0
358
- step = self.slice.step or 1
359
- for obj in await self.env.getitem_async(match.obj, self.slice):
360
- norm_index = self._normalized_index(match.obj, idx)
357
+ for norm_index, obj in zip( # noqa: B905
358
+ range(*self.slice.indices(len(match.obj))),
359
+ await self.env.getitem_async(match.obj, self.slice),
360
+ ):
361
361
  _match = self.env.match_class(
362
362
  filter_context=match.filter_context(),
363
363
  obj=obj,
@@ -368,7 +368,6 @@ class SliceSelector(JSONPathSelector):
368
368
  )
369
369
  match.add_child(_match)
370
370
  yield _match
371
- idx += step
372
371
 
373
372
 
374
373
  class WildSelector(JSONPathSelector):
@@ -402,7 +401,7 @@ class WildSelector(JSONPathSelector):
402
401
  obj=val,
403
402
  parent=match,
404
403
  parts=match.parts + (key,),
405
- path=match.path + f"['{key}']",
404
+ path=match.path + f"[{canonical_string(key)}]",
406
405
  root=match.root,
407
406
  )
408
407
  match.add_child(_match)
@@ -431,7 +430,7 @@ class WildSelector(JSONPathSelector):
431
430
  obj=val,
432
431
  parent=match,
433
432
  parts=match.parts + (key,),
434
- path=match.path + f"['{key}']",
433
+ path=match.path + f"[{canonical_string(key)}]",
435
434
  root=match.root,
436
435
  )
437
436
  match.add_child(_match)
@@ -479,7 +478,7 @@ class RecursiveDescentSelector(JSONPathSelector):
479
478
  obj=val,
480
479
  parent=match,
481
480
  parts=match.parts + (key,),
482
- path=match.path + f"['{key}']",
481
+ path=match.path + f"[{canonical_string(key)}]",
483
482
  root=match.root,
484
483
  )
485
484
  match.add_child(_match)
@@ -633,7 +632,7 @@ class Filter(JSONPathSelector):
633
632
  obj=val,
634
633
  parent=match,
635
634
  parts=match.parts + (key,),
636
- path=match.path + f"['{key}']",
635
+ path=match.path + f"[{canonical_string(key)}]",
637
636
  root=match.root,
638
637
  )
639
638
  match.add_child(_match)
@@ -701,7 +700,7 @@ class Filter(JSONPathSelector):
701
700
  obj=val,
702
701
  parent=match,
703
702
  parts=match.parts + (key,),
704
- path=match.path + f"['{key}']",
703
+ path=match.path + f"[{canonical_string(key)}]",
705
704
  root=match.root,
706
705
  )
707
706
  match.add_child(_match)
@@ -0,0 +1,13 @@
1
+ """Helper functions for serializing compiled JSONPath queries."""
2
+
3
+ import json
4
+
5
+
6
+ def canonical_string(value: str) -> str:
7
+ """Return _value_ as a canonically formatted string literal."""
8
+ single_quoted = (
9
+ json.dumps(value, ensure_ascii=False)[1:-1]
10
+ .replace('\\"', '"')
11
+ .replace("'", "\\'")
12
+ )
13
+ return f"'{single_quoted}'"
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
  name = "python-jsonpath"
7
7
  description = 'JSONPath, JSON Pointer and JSON Patch for Python.'
8
8
  readme = "README.md"
9
- requires-python = ">=3.7"
9
+ requires-python = ">=3.8"
10
10
  license = "MIT"
11
11
  keywords = []
12
12
  authors = [{ name = "James Prior", email = "jamesgr.prior@gmail.com" }]
@@ -15,7 +15,6 @@ classifiers = [
15
15
  "Intended Audience :: Developers",
16
16
  "License :: OSI Approved :: MIT License",
17
17
  "Programming Language :: Python",
18
- "Programming Language :: Python :: 3.7",
19
18
  "Programming Language :: Python :: 3.8",
20
19
  "Programming Language :: Python :: 3.9",
21
20
  "Programming Language :: Python :: 3.10",
@@ -67,7 +66,7 @@ lint = "ruff check ."
67
66
  typing = "mypy"
68
67
 
69
68
  [[tool.hatch.envs.test.matrix]]
70
- python = ["37", "38", "39", "310", "311", "312", "pypy38", "pypy39"]
69
+ python = ["38", "39", "310", "311", "312", "pypy38", "pypy39"]
71
70
 
72
71
  [tool.hatch.envs.test.scripts]
73
72
  test = "pytest {args}"
@@ -168,7 +167,7 @@ select = [
168
167
  "YTT",
169
168
  ]
170
169
 
171
- ignore = ["S105", "S101", "D107", "D105", "PLR0913", "SIM108", "PT001"]
170
+ ignore = ["S105", "S101", "D107", "D105", "PLR0913", "SIM108", "PT001", "A005"]
172
171
 
173
172
  fixable = ["I"]
174
173
  unfixable = []