diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 991d073..e437415 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -# This file is autogenerated by maturin v1.3.1 +# This file is autogenerated by maturin v1.8.6 # To update, run # # maturin generate-ci github @@ -20,71 +20,122 @@ permissions: jobs: linux: - runs-on: ubuntu-latest + runs-on: ${{ matrix.platform.runner }} strategy: matrix: - target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + - runner: ubuntu-22.04 + target: s390x + - runner: ubuntu-22.04 + target: ppc64le steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: 3.x - name: Build wheels uses: PyO3/maturin-action@v1 with: - target: ${{ matrix.target }} + target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter - sccache: 'true' + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: auto - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-linux-${{ matrix.target }} + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: musllinux_1_2 + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-musllinux-${{ matrix.platform.target }} path: dist windows: - runs-on: windows-latest + runs-on: ${{ matrix.platform.runner }} strategy: matrix: - target: [x64, x86] + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.10' - architecture: ${{ matrix.target }} + python-version: 3.x + architecture: ${{ matrix.platform.target }} - name: Build wheels uses: PyO3/maturin-action@v1 with: - target: ${{ matrix.target }} + target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter - sccache: 'true' + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-windows-${{ matrix.target }} + name: wheels-windows-${{ matrix.platform.target }} path: dist macos: - runs-on: macos-latest + runs-on: ${{ matrix.platform.runner }} strategy: matrix: - target: [x86_64, aarch64] + platform: + - runner: macos-13 + target: x86_64 + - runner: macos-14 + target: aarch64 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: 3.x - name: Build wheels uses: PyO3/maturin-action@v1 with: - target: ${{ matrix.target }} + target: ${{ matrix.platform.target }} args: --release --out dist --find-interpreter - sccache: 'true' + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-macos-${{ matrix.target }} + name: wheels-macos-${{ matrix.platform.target }} path: dist sdist: @@ -105,18 +156,26 @@ jobs: release: name: Release runs-on: ubuntu-latest - if: "startsWith(github.ref, 'refs/tags/')" - needs: [linux, windows, macos, sdist] + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} + needs: [linux, musllinux, windows, macos, sdist] + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write steps: - uses: actions/download-artifact@v4 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 with: - pattern: wheels-* - path: wheels - merge-multiple: true + subject-path: 'wheels-*/*' - name: Publish to PyPI + if: ${{ startsWith(github.ref, 'refs/tags/') }} uses: PyO3/maturin-action@v1 env: MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} with: command: upload - args: --non-interactive --skip-existing wheels/* \ No newline at end of file + args: --non-interactive --skip-existing wheels-*/* diff --git a/Cargo.lock b/Cargo.lock index 5837f88..1cd9389 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,20 +91,20 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jsonpath-rust" -version = "0.7.5" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b" +checksum = "5b37465feaf9d41f74df7da98c6c1c31ca8ea06d11b5bf7869c8f1ccc51a793f" dependencies = [ "pest", "pest_derive", "regex", "serde_json", - "thiserror 2.0.12", + "thiserror", ] [[package]] name = "jsonpath_rust_bindings" -version = "0.7.5" +version = "1.0.2" dependencies = [ "jsonpath-rust", "pyo3", @@ -141,20 +141,20 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "pest" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", - "thiserror 1.0.50", + "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" dependencies = [ "pest", "pest_generator", @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" dependencies = [ "pest", "pest_meta", @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", @@ -201,11 +201,10 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4" dependencies = [ - "cfg-if", "indoc", "libc", "memoffset", @@ -219,9 +218,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d" dependencies = [ "once_cell", "target-lexicon", @@ -229,9 +228,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e" dependencies = [ "libc", "pyo3-build-config", @@ -239,9 +238,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -251,9 +250,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e" dependencies = [ "heck", "proc-macro2", @@ -264,9 +263,9 @@ dependencies = [ [[package]] name = "pythonize" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bcac0d0b71821f0d69e42654f1e15e5c94b85196446c4de9588951a2117e7b" +checksum = "597907139a488b22573158793aa7539df36ae863eba300c75f3a0d65fc475e27" dependencies = [ "pyo3", "serde", @@ -375,33 +374,13 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" -[[package]] -name = "thiserror" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" -dependencies = [ - "thiserror-impl 1.0.50", -] - [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3e919fa..dee4970 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonpath_rust_bindings" -version = "0.7.5" +version = "1.0.2" edition = "2021" [lib] @@ -8,7 +8,7 @@ name = "jsonpath_rust_bindings" crate-type = ["cdylib"] [dependencies] -pyo3 = "0.24" -jsonpath-rust = "0.7.5" +pyo3 = "0.25.0" +jsonpath-rust = "1.0.2" serde_json = "1.0" -pythonize = "0.24.0" +pythonize = "0.25.0" diff --git a/README.md b/README.md index 7c3a5e5..87f9db2 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ for query in queries: - data: the found value - path: the path to the found value -- is_new_value: whether the value is a new value or a copy of the original value `JsonPathResult` can't be constructed from Python; it is only returned by `Finder.find()`. @@ -110,3 +109,26 @@ Also, It has yet another consequence demonstrated in the following example: >>> original_object_i_want_to_mutate {'a': {'b': 'sample b'}} ``` + +## Development + +Clone the repository. + +Update maturin if needed: + +```bash +pip install maturin -U +``` + +Add your changes, then run: + +```bash +maturin develop +``` + + +Update CI: + +```bash +maturin generate-ci github > ./.github/workflows/CI.yml +``` diff --git a/jsonpath_rust_bindings.pyi b/jsonpath_rust_bindings.pyi index 6387894..1283aa7 100644 --- a/jsonpath_rust_bindings.pyi +++ b/jsonpath_rust_bindings.pyi @@ -8,9 +8,6 @@ class JsonPathResult: @property def path(self) -> Optional[str]: ... - @property - def is_new_value(self) -> bool: ... - class Finder: def __init__( @@ -19,4 +16,3 @@ class Finder: ) -> None: ... def find(self, query: str) -> List[JsonPathResult]: ... - def find_non_empty(self, query: str) -> List[JsonPathResult]: ... diff --git a/src/lib.rs b/src/lib.rs index d0bec33..690821b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ -use jsonpath_rust::{JsonPath, JsonPathValue}; +use jsonpath_rust::parser::model::JpQuery; +use jsonpath_rust::parser::parse_json_path; +use jsonpath_rust::query::{js_path_process, QueryRef}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pythonize::{depythonize, pythonize}; @@ -11,9 +13,6 @@ struct JsonPathResult { #[pyo3(get)] path: Option, - - #[pyo3(get)] - is_new_value: bool, } #[pymethods] @@ -40,53 +39,44 @@ impl Finder { fn find(self_: PyRef<'_, Self>, query: String) -> PyResult> { find_internal(&self_.value, &query, |_| true) } - - fn find_non_empty(self_: PyRef<'_, Self>, query: String) -> PyResult> { - find_internal(&self_.value, &query, |v| match v { - JsonPathValue::Slice(_, _) => true, - JsonPathValue::NewValue(_) => true, - JsonPathValue::NoValue => false, - }) - } } fn find_internal( value: &Value, query: &str, - predicate: impl Fn(&JsonPathValue) -> bool, + predicate: impl Fn(&QueryRef) -> bool, ) -> PyResult> { let query = parse_query(query)?; - let slice = query.find_slice(value).into_iter().filter(predicate); + let processed = match js_path_process(&query, value) { + Ok(p) => p, + Err(err) => { + return Err(PyValueError::new_err(err.to_string())); + } + }; + let filtered = processed.into_iter().filter(predicate); + Python::with_gil(|py| { - slice + filtered .into_iter() .map(|v| map_json_path_value(py, v)) .collect() }) } -fn map_json_path_value(py: Python, jpv: JsonPathValue) -> PyResult { - Ok(match jpv { - JsonPathValue::Slice(data, path) => JsonPathResult { - data: Some(pythonize(py, data)?.into_pyobject(py)?.unbind()), - path: Some(path.to_string()), - is_new_value: false, - }, - JsonPathValue::NewValue(data) => JsonPathResult { - data: Some(pythonize(py, &data)?.into_pyobject(py)?.unbind()), - path: None, - is_new_value: true, - }, - JsonPathValue::NoValue => JsonPathResult { - data: None, - path: None, - is_new_value: false, - }, - }) +fn map_json_path_value(py: Python, jpv: QueryRef) -> PyResult { + let path = jpv.clone().path(); + let val = jpv.val(); + + let res = JsonPathResult { + data: Some(pythonize(py, val)?.into_pyobject(py)?.unbind()), + path: Some(path), + }; + + Ok(res) } -fn parse_query(query: &str) -> PyResult { - match JsonPath::try_from(query) { +fn parse_query(query: &str) -> PyResult { + match parse_json_path(query) { Ok(inst) => Ok(inst), Err(err) => Err(PyValueError::new_err(format!("{err:?}"))), } @@ -112,8 +102,7 @@ fn repr_json_path_result(slf: PyRef<'_, JsonPathResult>) -> PyResult { None => "None", }; Ok(format!( - "JsonPathResult(data={data_repr}, path={path_repr:?}, is_new_value={})", - if slf.is_new_value { "True" } else { "False" } + "JsonPathResult(data={data_repr}, path={path_repr:?})", )) } diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 1830266..30a2d3b 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -12,32 +12,35 @@ def sample_data() -> dict: 'category': 'reference', 'author': 'Nigel Rees', 'title': 'Sayings of the Century', - 'price': 8.95, + 'price': 8.95 }, { 'category': 'fiction', 'author': 'Evelyn Waugh', 'title': 'Sword of Honour', - 'price': 12.99, + 'price': 12.99 }, { 'category': 'fiction', 'author': 'Herman Melville', 'title': 'Moby Dick', 'isbn': '0-553-21311-3', - 'price': 8.99, + 'price': 8.99 }, { 'category': 'fiction', 'author': 'J. R. R. Tolkien', 'title': 'The Lord of the Rings', 'isbn': '0-395-19395-8', - 'price': 22.99, - }, + 'price': 22.99 + } ], - 'bicycle': {'color': 'red', 'price': 19.95}, + 'bicycle': { + 'color': 'red', + 'price': 19.95 + } }, - 'expensive': 10, + 'expensive': 10 } @@ -58,18 +61,39 @@ def test_exceptions(sample_data): def test_repr(sample_data): finder = Finder(sample_data) result = str(finder.find('$.store.bicycle.color')[0]) - assert result == """JsonPathResult(data='red', path="$.['store'].['bicycle'].['color']", is_new_value=False)""" + assert result == """JsonPathResult(data='red', path="$['store']['bicycle']['color']")""" -def test_non_empty(sample_data): - finder = Finder(sample_data) +def test_smoke_queries(sample_data): + queries = [ + '$.store.book[*].author', + '$..book[?(@.isbn)]', + '$.store.*', + '$..author', + '$.store..price', + '$..book[2]', + '$..book[-2]', + '$..book[0,1]', + '$..book[:2]', + '$..book[1:2]', + '$..book[-2:]', + '$..book[2:]', + '$.store.book[?(@.price<10)]', + '$..book[?(@.price<=$.expensive)]', + # "$..book[?@.author ~= '(?i)REES']", + '$..*', + ] + + f = Finder(sample_data) + + for query in queries: + print(query) + print(f.find(query), '\n') + print('----------') - result = finder.find("..*[?(@.category=='fiction')]") - for entry in result: - assert entry.path is not None and entry.data is not None - # after jsonpath-rust version bump this method does not seem to be needed, - # though let it be for some time just in case - result = finder.find_non_empty("..*[?(@.category=='fiction')]") - for entry in result: - assert entry.path is not None and entry.data is not None +def test_overflow(): + big_number = 18446744005107584948 + f = Finder({"test": big_number}) + res = f.find('$.test')[0].data + assert res == big_number