Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ These breaking changes apply to Python JSONPath in its default configuration. We
- Slice selector indexes and step now follow the specification. Previously leading zeros and negative zero were allowed, now they raise a `JSONPathSyntaxError`.
- Whitespace is no longer allowed between a dot (`.` or `..`) and a name when using shorthand notation for the name selector. Whitespace before the dot oor double dot is OK.

**JSONPath function extension changes**

- The non-standard `keys()` function extension has been reimplemented. It used to be a simple Python function, `jsonpath.function_extensions.keys`. Now it is a "well-typed" class, `jsonpath.function_extensions.Keys`. See the [filter functions](https://jg-rp.github.io/python-jsonpath/functions/#keys) documentation.

**JSONPath features**

- Added the [Keys filter selector](https://jg-rp.github.io/python-jsonpath/syntax/#keys-filter-selector).
Expand Down
28 changes: 28 additions & 0 deletions docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,34 @@ And `is()` is an alias for `isinstance()`:
$.categories[?is(@.length, 'number')]
```

## `keys()`

**_New in version 2.0.0_**

```
keys(value: object) -> Tuple[str, ...] | Nothing
```

Return a list of keys from an object/mapping. If `value` does not have a `keys()` method, the special _Nothing_ value is returned.

!!! note

`keys()` is not registered with the default JSONPath environment. The [keys selector](syntax.md#keys-selector) and [keys filter selector](syntax.md#keys-filter-selector) are usually the better choice when strict compliance with the specification is not needed.

You can register `keys()` with your JSONPath environment like this:

```python
from jsonpath import JSONPathEnvironment
from jsonpath import function_extensions

env = JSONPathEnvironment()
env.function_extensions["keys"] = function_extensions.Keys()
```

```
$.some[?'thing' in keys(@)]
```

## `length()`

```text
Expand Down
4 changes: 2 additions & 2 deletions jsonpath/function_extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .filter_function import FilterFunction
from .count import Count
from .is_instance import IsInstance
from .keys import keys
from .keys import Keys
from .length import Length
from .match import Match
from .search import Search
Expand All @@ -16,7 +16,7 @@
"ExpressionType",
"FilterFunction",
"IsInstance",
"keys",
"Keys",
"Length",
"Match",
"Search",
Expand Down
35 changes: 27 additions & 8 deletions jsonpath/function_extensions/keys.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
"""The built-in `keys` function extension."""
"""The `keys` JSONPath filter function."""

from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import Union

from jsonpath.filter import UNDEFINED
from jsonpath.filter import _Undefined

from .filter_function import ExpressionType
from .filter_function import FilterFunction


class Keys(FilterFunction):
"""The `keys` JSONPath filter function."""

arg_types = [ExpressionType.VALUE]
return_type = ExpressionType.VALUE

def __call__(
self, value: Mapping[str, object]
) -> Union[Tuple[str, ...], _Undefined]:
"""Return a tuple of keys in `value`.

def keys(obj: Mapping[str, object]) -> Optional[Tuple[str, ...]]:
"""Return an object's keys, or `None` if the object has no _keys_ method."""
try:
return tuple(obj.keys())
except AttributeError:
return None
If `value` does not have a `keys()` method, the special _Nothing_ value
is returned.
"""
try:
return tuple(value.keys())
except AttributeError:
return UNDEFINED
62 changes: 62 additions & 0 deletions tests/test_keys_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import dataclasses
import operator
from typing import Any
from typing import Mapping
from typing import Sequence
from typing import Union

import pytest

from jsonpath import JSONPathEnvironment
from jsonpath import function_extensions


@dataclasses.dataclass
class Case:
description: str
path: str
data: Union[Sequence[Any], Mapping[str, Any]]
want: Union[Sequence[Any], Mapping[str, Any]]


SOME_OBJECT = object()

TEST_CASES = [
Case(
description="value in keys of an object",
path="$.some[?'thing' in keys(@)]",
data={"some": [{"thing": "foo"}]},
want=[{"thing": "foo"}],
),
Case(
description="value not in keys of an object",
path="$.some[?'else' in keys(@)]",
data={"some": [{"thing": "foo"}]},
want=[],
),
Case(
description="keys of an array",
path="$[?'thing' in keys(@)]",
data={"some": [{"thing": "foo"}]},
want=[],
),
Case(
description="keys of an string value",
path="$some[0].thing[?'else' in keys(@)]",
data={"some": [{"thing": "foo"}]},
want=[],
),
]


@pytest.fixture()
def env() -> JSONPathEnvironment:
_env = JSONPathEnvironment()
_env.function_extensions["keys"] = function_extensions.Keys()
return _env


@pytest.mark.parametrize("case", TEST_CASES, ids=operator.attrgetter("description"))
def test_isinstance_function(env: JSONPathEnvironment, case: Case) -> None:
path = env.compile(case.path)
assert path.findall(case.data) == case.want
Loading