Skip to content

Commit a1938e2

Browse files
markborkumdmlb2000
authored andcommitted
Implement JSONPath (#8)
* Implement JSONPath Copy files from https://github.com/markborkum/pacifica-jsonpath@jsonpath2 and merge conflicts. * Fix some Flake8 warnings * Initial pre-commit run Signed-off-by: David Brown <[email protected]> * Test parse_str method. * Operator callables should be static Flake8 warned that the operator callables were globals. In a previous commit, the operator callables were refactored into method-locals in the operator's constructor. This is sufficient to resolve the flake8 warnings, but introduces a bug: since callables are created per instance, they have different memory locations, and hence, are not compatible with the `Node`-level definition of `__eq__`, which naively compares each `__dict__` of each operand. The fix is to refactor the operator callables into static methods. * pre-commit fixes * add some bookstore examples for testing Signed-off-by: David Brown <[email protected]> * pre-commit fixes Signed-off-by: David Brown <[email protected]> * Rollback modification to grammar * Fix bookstore tests In previous implementations of JSONPath, the current value is implicitly cast to array or object if the next node is an array index subscript or object index subscript. This introduces ambiguity: "is the index subscript referring to the current value, or a child value of the current value?" The solution is to use the wildcard "*" to refer to the child values explicitly. * add more testing to increase coverage Signed-off-by: David Brown <[email protected]> * pre-commit fix Signed-off-by: David Brown <[email protected]> * more testing coverage * Improve slice notation Aim for feature parity with equivalent in Python programming language * Tests to increase coverage * pre-commit fixes * fix pre-commit * try some more non-sense tests * add array to test * Python 3.5 doesn't seem to be working great * Add coverage pragmas * More coverage tests * try getting pre-commit and testing right
1 parent 27f697d commit a1938e2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+4180
-68
lines changed

.flake8

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[flake8]
2+
# 79 chars is too strict and we don't have 80-char terminals nowadays,
3+
# 160 chars is too much since it doesn't let us use split view efficiently:
4+
max-line-length = 120

.github/ISSUE_TEMPLATE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
### JSONPath2 version
2-
[Version of the JSONPath2 software where you are encountering the issue]
1+
### jsonpath2 version
2+
[Version of jsonpath2 where you are encountering the issue]
33

44
### Platform Details
55
[Operating system distribution and release version. Cloud provider if running in the cloud]

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
exclude: 'jsonpath2/parser/JSONPath.*'
12
repos:
23
- repo: git://github.com/pre-commit/pre-commit-hooks
34
rev: v1.2.3

.travis.yml

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
language: python
2-
python:
3-
- 2.7
4-
- 3.6
2+
python: 3.6
53
stages:
64
- lint
75
- test
86
- deploy
97

108
install: pip install -r requirements-dev.txt
11-
script:
12-
- coverage run --include='jsonpath2/*' -m pytest -v
13-
- coverage report -m --fail-under 100
14-
- pip install .
15-
- python setup.py bdist_wheel
16-
- python setup.py sdist
179
jobs:
1810
include:
1911
- stage: lint
2012
python: 3.6
2113
script: pre-commit run -a
22-
- python: 2.7
23-
script: pre-commit run -a
14+
- stage: test
15+
python: 3.6
16+
script:
17+
- coverage run --include='jsonpath2/*' --omit='jsonpath2/parser/JSONPath*' -m pytest -v
18+
- coverage report -m --fail-under 100
19+
- pip install .
20+
- python setup.py bdist_wheel
21+
- python setup.py sdist
2422
- stage: deploy
2523
python: 3.6
2624
script: skip
@@ -33,14 +31,3 @@ jobs:
3331
distributions: "sdist bdist_wheel"
3432
on:
3533
tags: true
36-
- python: 2.7
37-
script: skip
38-
deploy:
39-
skip_cleanup: true
40-
provider: pypi
41-
user: dmlb2000
42-
password:
43-
secure: MeskzH+TIGe4iboe/VL0+3dSJ5yL/0f8CVH7AcLBmToEArWAOdx5v42fDbOGxSio9ezYdlGyP1fTeBZybIhCvnv44W43iXol2DSkNILdIkfPhsp/RWvZh+qylSldfwiS+gKRtWRCnMpItpmIDMpbBBf/malDLgo41JrhUMeJ2EgvAlRAIDN58VcgZFCyq/cYpo8aRnqvjAmHKwNwEVZP9fFttpys7JXnxxXgP66Yr7WZIVp1v3wv5KwJdqdLlWAL/ZDftTy61ad23sZn0sv3DWYRJ8eJxb2UXQssLyhoZDvAKFoymFhBWoNINpwYDkTZeSQkRPuf1BHgSYRe3nT+71IpXIBF0H7kbmStOttk2Z2kPrlprkZhoZlUwYhRmwgTKWPR2BCyzepDfNKFGoGLz1a98ymb/iqJbBhtuo2ZHH6xsodfKmjVRS8Cx6xCXYyUG5ZW9NK/luMYSNmM78vL6HNcY+yGZ1GS6kXtjUVLPh9CSXld6fuDY/sWWzpXWuhutbfM8+TKNXNF/JOnolJVAgpseDLW3rlNM8jKFLYv1ut/MR/qyoTeMzGe03BgMxX4o5LesVHaWQfvlDubCnXmeRdgYWuxGmFCmRphIu7d3+NwI/ZWWV6dhlqdID1YbdmQJcfz/NPslAn3sXvgLpsmiuSyr2FIuXBbhQozc+xstsQ=
44-
distributions: "bdist_wheel"
45-
on:
46-
tags: true

README.md

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,142 @@
1-
# Python JSONPath2
1+
# jsonpath2
22
[![Build Status](https://travis-ci.org/pacifica/python-jsonpath2.svg?branch=master)](https://travis-ci.org/pacifica/python-jsonpath2)
3-
A JSONPath implementation for Python (but better than jsonpath).
3+
4+
This repository contains an implementation of [JSONPath](http://goessner.net/articles/JsonPath/) ([XPath](https://www.w3.org/TR/xpath/all/) for [JSON](https://www.json.org/)) for the Python programming language.
5+
6+
## API
7+
8+
### `Path` class
9+
10+
The `jsonpath2.Path.Path` class represents a JSONPath.
11+
12+
```python
13+
>>> s = '{"hello":"Hello, world!"}'
14+
'{"hello":"Hello, world!"}'
15+
>>> import json
16+
>>> d = json.loads(s)
17+
{'hello':'Hello, world!'}
18+
>>> from jsonpath2.path import Path
19+
>>> p = Path.parse_str('$["hello"]')
20+
<jsonpath2.path.Path object>
21+
>>> list(map(lambda match_data: match_data.current_value, p.match(d)))
22+
['Hello, world!']
23+
>>> list(map(lambda match_data: match_data.node.tojsonpath(), p.match(d)))
24+
['$["hello"]']
25+
```
26+
27+
This class is constructed with respect to the given instance of the `jsonpath2.Path.RootNode` class (viz., the `root_node` property).
28+
29+
#### `parse_str(strdata)` class method
30+
31+
Parse the given string and return a new instance of this class.
32+
33+
#### `parse_file(fileName, encoding='ascii')` class method
34+
35+
Parse the contents of the given file and return a new instance of this class.
36+
37+
#### `match(root_value)` instance method
38+
39+
Match the given JSON data structure against this instance.
40+
For each match, yield an instance of the `jsonpath2.Node.MatchData` class.
41+
42+
#### `__eq__(other)` instance method
43+
44+
Tests if two instances are equal.
45+
46+
#### `__str__()` instance method
47+
48+
Returns the string representation of this instance.
49+
50+
#### `root_node` property
51+
52+
The root node of the abstract syntax tree for this instance.
53+
54+
### `Node` abstract class
55+
56+
The `jsonpath2.Node.Node` class represents the abstract syntax tree for a JSONPath.
57+
58+
#### `__eq__(other)` instance method
59+
60+
Tests if two instances are equal.
61+
62+
#### `__jsonpath__()` instance method
63+
64+
Yields the lexer tokens for the string representation of this instance.
65+
66+
#### `match(root_value, current_value)` instance method
67+
68+
Match the given root and current JSON data structures against this instance.
69+
For each match, yield an instance of the `jsonpath2.Node.MatchData` class.
70+
71+
#### `tojsonpath()` instance method
72+
73+
Returns the string representation of this instance.
74+
75+
### `MatchData` class
76+
77+
The `jsonpath2.Node.MatchData` class represents the JSON value and context for a JSONPath match.
78+
79+
This class is constructed with respect to a root JSON value, a current JSON value, and an abstract syntax tree node.
80+
81+
#### `__eq__(other)` instance method
82+
83+
Tests if two instances are equal.
84+
85+
#### `root_value` property
86+
87+
The root JSON value.
88+
89+
#### `current_value` property
90+
91+
The current JSON value (i.e., the matching JSON value).
92+
93+
#### `node` property
94+
95+
The abstract syntax tree node.
96+
97+
## Syntax
98+
99+
| XPath | JSONPath | Description |
100+
| - | - | - |
101+
| `/` | `$` | the root JSON value |
102+
| `.` | `@` | the current JSON value |
103+
| `/` | `.` or `[]` | child operator |
104+
| `//` | `..` | recursive descent (depth-first search) |
105+
| `*` | `*` | wildcard (all elements of a JSON array; all values of a JSON object; otherwise none) |
106+
| `[]` | `[]` | subscript operator |
107+
| <code>&#124;</code> | `[,]` | union operator (for two or more subscript operators) |
108+
| n/a | `[start:end:step]` | slice operator (subset of elements of a JSON array) |
109+
| `[]` | `?()` | filter expression (for use with subscript operator) |
110+
111+
| JSONPath Filter Expression | Description |
112+
| - | - |
113+
| `$` or `@` | nested JSONPath (returns `true` if any match exists; otherwise, returns `false`) |
114+
| `=`, `!=`, `>`, `>=`, `<`, `<=` | binary operator, where left-hand operand is a nested JSONPath and right-right operand is a JSON value (returns `true` if any match exists; otherwise, returns `false`) |
115+
| `and`, `or`, `not` | Boolean operator, where operands are JSONPath filter expressions |
116+
| `(` ... `)` | parentheses |
117+
118+
## Grammar and parser
119+
120+
The [ANTLR v4](https://github.com/antlr/antlr4) grammar for JSONPath is available at `jsonpath2/parser/JSONPath.g4`.
121+
122+
### Installing ANTLR v4
123+
124+
Adapted from https://github.com/antlr/antlr4/blob/master/doc/getting-started.md.
125+
126+
```bash
127+
cd /usr/local/lib
128+
curl -O http://www.antlr.org/download/antlr-4.7.1-complete.jar
129+
130+
export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH"
131+
132+
alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" org.antlr.v4.Tool'
133+
alias grun='java org.antlr.v4.gui.TestRig'
134+
```
135+
136+
### Building the parser for the grammar
137+
138+
Adapted from https://github.com/antlr/antlr4/blob/master/doc/python-target.md.
139+
140+
```bash
141+
antlr4 -Dlanguage=Python3 -o . -lib . jsonpath2/parser/JSONPath.g4
142+
```

jsonpath2/__init__.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,3 @@
11
#!/usr/bin/python
22
# -*- coding: utf-8 -*-
3-
"""Example Module."""
4-
5-
6-
class Example(object):
7-
"""This is an example class in the example module."""
8-
9-
@staticmethod
10-
def add(thing1, thing2):
11-
"""Add thing one and thing two together."""
12-
return thing1 + thing2
13-
14-
@staticmethod
15-
def mul(thing1, thing2):
16-
"""Multiply thing one and thing two together."""
17-
return thing1 * thing2
3+
"""The jsonpath2 module."""

jsonpath2/expression.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
"""Expression module."""
4+
from abc import abstractmethod
5+
from jsonpath2.tojsonpath import ToJSONPath
6+
7+
8+
class Expression(ToJSONPath):
9+
"""Add the expression methods to the jsonpath object."""
10+
11+
def __eq__(self, other: object) -> bool:
12+
"""Test self the same as the other object."""
13+
return isinstance(other, self.__class__) and (self.__dict__ == other.__dict__)
14+
15+
@abstractmethod
16+
def evaluate(self, root_value: object, current_value: object) -> bool: # pragma: no cover abstract method
17+
"""Abstract method to evaluate the expression."""
18+
raise NotImplementedError()

jsonpath2/expressions/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
"""Expressions used in jsonpath module."""

0 commit comments

Comments
 (0)