Skip to content

Commit 330e154

Browse files
committed
feat: add typeof() and isinstance(), closes #4
1 parent db25182 commit 330e154

File tree

8 files changed

+359
-5
lines changed

8 files changed

+359
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
**Features**
66

7-
- Added the keys/properties selector (`~`).
7+
- Added a non-standard keys/properties selector (`~`).
8+
- Added a non-standard `typeof()` filter function. `type()` is an alias for `typeof()`.
9+
- Added a non-standard `isinstance()` filter function. `is()` is an alias for `isinstance()`.
810

911
**IETF JSONPath Draft compliance**
1012

docs/functions.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Filter Functions
22

3-
A filter function is a named function that can be called as part of a [filter selector](syntax.md#filters-expression) expression. Here we describe the standard, built-in filters. You can [define your own function extensions](advanced.md#function-extensions) too.
3+
A filter function is a named function that can be called as part of a [filter selector](syntax.md#filters-expression) expression. Here we describe built-in filters. You can [define your own function extensions](advanced.md#function-extensions) too.
44

55
## `count()`
66

@@ -14,6 +14,39 @@ Return the number of items in _obj_. If the object does not respond to Python's
1414
$.categories[?count(@.products.*) >= 2]
1515
```
1616

17+
## `isinstance()`
18+
19+
**_New in version 0.6.0_**
20+
21+
```text
22+
isinstance(obj: object, t: str) -> bool
23+
```
24+
25+
Return `True` if the type of _obj_ matches _t_. This function allows _t_ to be one of several aliases for the real Python "type". Some of these aliases follow JavaScript/JSON semantics.
26+
27+
| type | aliases |
28+
| --------------------- | ----------------------------- |
29+
| UNDEFINED | "undefined" |
30+
| None | "null", "nil", "None", "none" |
31+
| str | "str", "string" |
32+
| Sequence (array-like) | "array", "list", "sequence" |
33+
| Mapping (dict-like) | "object", "dict", "mapping" |
34+
| bool | "bool", "boolean" |
35+
| int | "number", "int" |
36+
| float | "number", "float" |
37+
38+
For example :
39+
40+
```
41+
$.categories[?isinstance(@.length, 'number')]
42+
```
43+
44+
And `is()` is an alias for `isinstance()`:
45+
46+
```
47+
$.categories[?is(@.length, 'number')]
48+
```
49+
1750
## `length()`
1851

1952
```text
@@ -58,6 +91,22 @@ If _pattern_ is a string literal, it will be compiled at compile time, and raise
5891

5992
If _pattern_ is a query and the result is not a valid regex, `False` is returned.
6093

94+
## `typeof()`
95+
96+
**_New in version 0.6.0_**
97+
98+
```text
99+
typeof(obj: object) -> str
100+
```
101+
102+
Return the type of _obj_ as a string. The strings returned from this function use JavaScript/JSON terminology like "string", "array" and "object", much like the result of JavaScript's `typeof` operator.
103+
104+
```
105+
$.categories[?typeof(@.length) == 'number']
106+
```
107+
108+
`type()` is and alias for `typeof()`.
109+
61110
## `value()`
62111

63112
```

jsonpath/env.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ def setup_function_extensions(self) -> None:
252252
self.function_extensions["match"] = function_extensions.Match()
253253
self.function_extensions["search"] = function_extensions.Search()
254254
self.function_extensions["value"] = function_extensions.value
255+
self.function_extensions["isinstance"] = function_extensions.IsInstance()
256+
self.function_extensions["is"] = self.function_extensions["isinstance"]
257+
self.function_extensions["typeof"] = function_extensions.TypeOf()
258+
self.function_extensions["type"] = self.function_extensions["typeof"]
255259

256260
def validate_function_extension_signature(
257261
self, token: Token, args: List[Any]
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
# noqa: D104
22
from .arguments import validate
33
from .count import Count
4+
from .is_instance import IsInstance
45
from .keys import keys
56
from .length import length
67
from .match import Match
78
from .search import Search
9+
from .typeof import TypeOf
810
from .value import value
911

1012
__all__ = (
1113
"Count",
12-
"Match",
13-
"Search",
14-
"value",
14+
"IsInstance",
1515
"keys",
1616
"length",
17+
"Match",
18+
"Search",
19+
"TypeOf",
1720
"validate",
21+
"value",
1822
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""A non-standard "isinstance" filter function."""
2+
3+
from typing import Mapping
4+
from typing import Sequence
5+
6+
from ..filter import UNDEFINED
7+
from ..filter import UNDEFINED_LITERAL
8+
9+
10+
class IsInstance:
11+
"""A non-standard "isinstance" filter function."""
12+
13+
def __call__(self, obj: object, t: str) -> bool: # noqa: PLR0911
14+
"""Return `True` if the type of _obj_ matches _t_.
15+
16+
This function allows _t_ to be one of several aliases for the real
17+
Python "type". Some of these aliases follow JavaScript/JSON semantics.
18+
"""
19+
if obj is UNDEFINED or obj is UNDEFINED_LITERAL:
20+
return t == "undefined"
21+
if obj is None:
22+
return t in ("null", "nil", "None", "none")
23+
if isinstance(obj, str):
24+
return t in ("str", "string")
25+
if isinstance(obj, Sequence):
26+
return t in ("array", "list", "sequence")
27+
if isinstance(obj, Mapping):
28+
return t in ("object", "dict", "mapping")
29+
if isinstance(obj, bool):
30+
return t in ("bool", "boolean")
31+
if isinstance(obj, int):
32+
return t in ("number", "int")
33+
if isinstance(obj, float):
34+
return t in ("number", "float")
35+
return t == "object"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""A non-standard "typeof" filter function."""
2+
3+
from typing import Mapping
4+
from typing import Sequence
5+
6+
from ..filter import UNDEFINED
7+
from ..filter import UNDEFINED_LITERAL
8+
9+
10+
class TypeOf:
11+
"""A non-standard "typeof" filter function.
12+
13+
Arguments:
14+
single_number_type: If True, will return "number" for ints and floats,
15+
otherwise we'll use "int" and "float" respectively. Defaults to `True`.
16+
"""
17+
18+
def __init__(self, *, single_number_type: bool = True) -> None:
19+
self.single_number_type = single_number_type
20+
21+
def __call__(self, obj: object) -> str: # noqa: PLR0911
22+
"""Return the type of _obj_ as a string.
23+
24+
The strings returned from this function use JSON terminology, much
25+
like the result of JavaScript's `typeof` operator.
26+
"""
27+
if obj is UNDEFINED or obj is UNDEFINED_LITERAL:
28+
return "undefined"
29+
if obj is None:
30+
return "null"
31+
if isinstance(obj, str):
32+
return "string"
33+
if isinstance(obj, Sequence):
34+
return "array"
35+
if isinstance(obj, Mapping):
36+
return "object"
37+
if isinstance(obj, bool):
38+
return "boolean"
39+
if isinstance(obj, int):
40+
return "number" if self.single_number_type else "int"
41+
if isinstance(obj, float):
42+
return "number" if self.single_number_type else "float"
43+
return "object"

tests/test_isinstance_function.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import asyncio
2+
import dataclasses
3+
import operator
4+
from typing import Any
5+
from typing import List
6+
from typing import Mapping
7+
from typing import Sequence
8+
from typing import Union
9+
10+
import pytest
11+
12+
from jsonpath import JSONPathEnvironment
13+
14+
15+
@dataclasses.dataclass
16+
class Case:
17+
description: str
18+
path: str
19+
data: Union[Sequence[Any], Mapping[str, Any]]
20+
want: Union[Sequence[Any], Mapping[str, Any]]
21+
22+
23+
SOME_OBJECT = object()
24+
25+
TEST_CASES = [
26+
Case(
27+
description="type of a string",
28+
path="$.some[?is(@.thing, 'string')]",
29+
data={"some": [{"thing": "foo"}]},
30+
want=[{"thing": "foo"}],
31+
),
32+
Case(
33+
description="not a string",
34+
path="$.some[?is(@.thing, 'string')]",
35+
data={"some": [{"thing": 1}]},
36+
want=[],
37+
),
38+
Case(
39+
description="type of undefined",
40+
path="$.some[?is(@.other, 'undefined')]", # things without `other`
41+
data={"some": [{"thing": "foo"}]},
42+
want=[{"thing": "foo"}],
43+
),
44+
Case(
45+
description="type of None",
46+
path="$.some[?is(@.thing, 'null')]",
47+
data={"some": [{"thing": None}]},
48+
want=[{"thing": None}],
49+
),
50+
Case(
51+
description="type of array-like",
52+
path="$.some[?is(@.thing, 'array')]",
53+
data={"some": [{"thing": [1, 2, 3]}]},
54+
want=[{"thing": [1, 2, 3]}],
55+
),
56+
Case(
57+
description="type of mapping",
58+
path="$.some[?is(@.thing, 'object')]",
59+
data={"some": [{"thing": {"other": 1}}]},
60+
want=[{"thing": {"other": 1}}],
61+
),
62+
Case(
63+
description="type of bool",
64+
path="$.some[?is(@.thing, 'boolean')]",
65+
data={"some": [{"thing": True}]},
66+
want=[{"thing": True}],
67+
),
68+
Case(
69+
description="type of int",
70+
path="$.some[?is(@.thing, 'number')]",
71+
data={"some": [{"thing": 1}]},
72+
want=[{"thing": 1}],
73+
),
74+
Case(
75+
description="type of float",
76+
path="$.some[?is(@.thing, 'number')]",
77+
data={"some": [{"thing": 1.1}]},
78+
want=[{"thing": 1.1}],
79+
),
80+
Case(
81+
description="none of the above",
82+
path="$.some[?is(@.thing, 'object')]",
83+
data={"some": [{"thing": SOME_OBJECT}]},
84+
want=[{"thing": SOME_OBJECT}],
85+
),
86+
]
87+
88+
89+
@pytest.fixture()
90+
def env() -> JSONPathEnvironment:
91+
return JSONPathEnvironment()
92+
93+
94+
@pytest.mark.parametrize("case", TEST_CASES, ids=operator.attrgetter("description"))
95+
def test_isinstance_function(env: JSONPathEnvironment, case: Case) -> None:
96+
path = env.compile(case.path)
97+
assert path.findall(case.data) == case.want
98+
99+
100+
@pytest.mark.parametrize("case", TEST_CASES, ids=operator.attrgetter("description"))
101+
def test_isinstance_function_async(env: JSONPathEnvironment, case: Case) -> None:
102+
path = env.compile(case.path)
103+
104+
async def coro() -> List[object]:
105+
return await path.findall_async(case.data)
106+
107+
assert asyncio.run(coro()) == case.want

0 commit comments

Comments
 (0)