diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7b511..bc59e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Python JSONPath Change Log +## Version 1.3.2 (unreleased) + +**Fixes** + +- Fixed JSONPath filter context data in embedded JSONPath queries. We were failing to pass on said context data when resolving embedded queries. See [#103](https://github.com/jg-rp/python-jsonpath/issues/103). + ## Version 1.3.1 **Fixes** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..828300a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,73 @@ +# Contributing to Python JSONPath + +Hi. Your contributions and questions are always welcome. Feel free to ask questions, report bugs or request features on the [issue tracker](https://github.com/jg-rp/python-jsonpath/issues) or on [Github Discussions](https://github.com/jg-rp/python-jsonpath/discussions). Pull requests are welcome too. + +**Table of contents** + +- [Development](#development) +- [Documentation](#documentation) +- [Style Guides](#style-guides) + +## Development + +The [JSONPath Compliance Test Suite](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite) and [JSONPath Normalized Path Test Suite](https://github.com/jg-rp/jsonpath-compliance-normalized-paths) are included in this repository as Git [submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules). Clone this project and initialize the submodules with something like: + +```shell +$ git clone git@github.com:jg-rp/python-jsonpath.git +$ cd python-jsonpath +$ git submodule update --init +``` + +We use [hatch](https://hatch.pypa.io/latest/) to manage project dependencies and development environments. + +Run tests with the _test_ script. + +```shell +$ hatch run test +``` + +Lint with [ruff](https://beta.ruff.rs/docs/). + +```shell +$ hatch run lint +``` + +Typecheck with [Mypy](https://mypy.readthedocs.io/en/stable/). + +```shell +$ hatch run typing +``` + +Check coverage with pytest-cov. + +```shell +$ hatch run cov +``` + +Or generate an HTML coverage report. + +```shell +$ hatch run cov-html +``` + +Then open `htmlcov/index.html` in your browser. + +## Documentation + +Documentation is currently in the [README](https://github.com/jg-rp/python-jsonpath/blob/main/README.md) and project source code only. + +## Style Guides + +### Git Commit Messages + +There are no hard rules for git commit messages, although you might like to indicate the type of commit by starting the message with `docs:`, `chore:`, `feat:`, `fix:` or `refactor:`, for example. + +### Python Style + +We use [Ruff](https://docs.astral.sh/ruff/) to lint and format all Python files. + +Ruff is configured to: + +- follow [Black](https://github.com/psf/black), with its default configuration. +- expect [Google style docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). +- enforce Python imports according to [isort](https://pycqa.github.io/isort/) with `force-single-line = true`. diff --git a/jsonpath/__about__.py b/jsonpath/__about__.py index 7ccd21a..64f778d 100644 --- a/jsonpath/__about__.py +++ b/jsonpath/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2023-present James Prior # # SPDX-License-Identifier: MIT -__version__ = "1.3.1" +__version__ = "1.3.2" diff --git a/jsonpath/filter.py b/jsonpath/filter.py index 00cbca4..0556cbf 100644 --- a/jsonpath/filter.py +++ b/jsonpath/filter.py @@ -544,7 +544,12 @@ def evaluate(self, context: FilterContext) -> object: return context.current return NodeList() - return NodeList(self.path.finditer(context.current)) + return NodeList( + self.path.finditer( + context.current, + filter_context=context.extra_context, + ) + ) async def evaluate_async(self, context: FilterContext) -> object: if isinstance(context.current, str): # TODO: refactor @@ -557,7 +562,13 @@ async def evaluate_async(self, context: FilterContext) -> object: return NodeList() return NodeList( - [match async for match in await self.path.finditer_async(context.current)] + [ + match + async for match in await self.path.finditer_async( + context.current, + filter_context=context.extra_context, + ) + ] ) @@ -576,11 +587,22 @@ def __str__(self) -> str: return str(self.path) def evaluate(self, context: FilterContext) -> object: - return NodeList(self.path.finditer(context.root)) + return NodeList( + self.path.finditer( + context.root, + filter_context=context.extra_context, + ) + ) async def evaluate_async(self, context: FilterContext) -> object: return NodeList( - [match async for match in await self.path.finditer_async(context.root)] + [ + match + async for match in await self.path.finditer_async( + context.root, + filter_context=context.extra_context, + ) + ] ) @@ -600,13 +622,21 @@ def __str__(self) -> str: return "_" + path_repr[1:] def evaluate(self, context: FilterContext) -> object: - return NodeList(self.path.finditer(context.extra_context)) + return NodeList( + self.path.finditer( + context.extra_context, + filter_context=context.extra_context, + ) + ) async def evaluate_async(self, context: FilterContext) -> object: return NodeList( [ match - async for match in await self.path.finditer_async(context.extra_context) + async for match in await self.path.finditer_async( + context.extra_context, + filter_context=context.extra_context, + ) ] ) diff --git a/tests/test_find.py b/tests/test_find.py index 6059641..140a291 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -127,18 +127,6 @@ class Case: } ], ), - Case( - description="issue 72, orders", - path="orders", - data={"orders": [1, 2, 3]}, - want=[[1, 2, 3]], - ), - Case( - description="issue 72, andy", - path="andy", - data={"andy": [1, 2, 3]}, - want=[[1, 2, 3]], - ), Case( description="quoted reserved word, and", path="['and']", diff --git a/tests/test_issues.py b/tests/test_issues.py new file mode 100644 index 0000000..3fe8e9b --- /dev/null +++ b/tests/test_issues.py @@ -0,0 +1,69 @@ +from jsonpath import findall + + +def test_issue_72_andy() -> None: + query = "andy" + data = {"andy": [1, 2, 3]} + assert findall(query, data) == [[1, 2, 3]] + + +def test_issue_72_orders() -> None: + query = "orders" + data = {"orders": [1, 2, 3]} + assert findall(query, data) == [[1, 2, 3]] + + +def test_issue_103() -> None: + query = "$..book[?(@.borrowers[?(@.name == _.name)])]" + data = { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95, + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99, + "borrowers": [ + {"name": "John", "id": 101}, + {"name": "Jane", "id": 102}, + ], + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + "borrowers": [{"name": "Peter", "id": 103}], + }, + ], + "bicycle": {"color": "red", "price": 19.95}, + } + } + + filter_context = {"name": "John"} + + want = [ + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99, + "borrowers": [{"name": "John", "id": 101}, {"name": "Jane", "id": 102}], + } + ] + + assert findall(query, data, filter_context=filter_context) == want