Skip to content

feat(cache): support register and switch pluggable cache backend#7683

Open
yanghua wants to merge 12 commits into
lance-format:mainfrom
yanghua:pluggable-cachebackend
Open

feat(cache): support register and switch pluggable cache backend#7683
yanghua wants to merge 12 commits into
lance-format:mainfrom
yanghua:pluggable-cachebackend

Conversation

@yanghua

@yanghua yanghua commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the phase-1 framework proposed in
discussion #7575:
a registry so cache backends can be selected by a stable string identifier
(kind) and configured through either a structured BackendConfig or a
compact URI. MokaCacheBackend is auto-registered under "moka" so no
user-visible behavior changes for the default configuration.

This PR intentionally ships only the mechanism. It does not add a new
concrete backend implementation. Third-party crates (compiled together with
Lance) integrate by defining their own BackendBuildFn and calling
register_backend(kind, build) once at startup. A future PR can add a
dynamic-library loader on top of the same registry without changing the
user-facing configuration model.

What lands

  1. BackendConfig + registry
    BackendConfig { kind, options: HashMap<String, String> } and a
    process-wide registry (register_backend, build_from_config). Options
    are FFI-friendly string pairs so the same shape carries across Python
    dict[str, str], Java Map<String, String>, or a future stable ABI.
    Duplicate kind registration returns an error rather than silently
    overriding an existing constructor.

  2. URI form
    parse_backend_uri / build_from_uri accept a compact single-string
    form (moka://?capacity=1073741824). Grammar is a small, predictable
    subset of RFC 3986:

    uri     ::= scheme ":" hier ( "?" query )?
    hier    ::= "//" authority path?  |  path
    

    The scheme becomes kind; the joined authority+path (if any) is stored
    under the option path; query pairs are percent-decoded. Duplicate query
    keys, pairs without =, and invalid schemes are rejected.

  3. Auto-register MokaCacheBackend as "moka"
    A private ensure_builtin_backends() runs on the first call to
    build_from_config / build_from_uri, so a bare Lance install can
    build a Moka backend from a URI/config without the caller registering
    it first. The build fn recognises a single option (capacity in bytes)
    and rejects unknown options so typos surface at construction time.

  4. Symmetric Session::with_cache_backends
    New constructor takes Option<Arc<dyn CacheBackend>> for both the index
    and metadata caches. Whichever tier the caller passes None for falls
    back to the same size-based default that Session::new would have used.
    The existing Session::new / Session::with_index_cache_backend
    entrypoints are unchanged.

  5. Python Session(index_cache_backend=..., metadata_cache_backend=...)
    Each accepts either a URI str ("moka://?capacity=...") resolved
    through build_from_uri, or a dict of the form
    {"kind": "moka", "options": {"capacity": "..."}} resolved through
    build_from_config. Passing both *_cache_size_bytes and
    *_cache_backend for the same cache raises ValueError at the binding
    boundary rather than silently letting one override the other
    (proposal §7).

What this PR intentionally does not do

  • No new concrete backend is added. Foyer / Redis / disk backends live in
    external crates and register themselves with register_backend(...).
  • No dynamic .so / C-ABI loader. The design keeps room for it later
    (string-map config, conflict-detecting registry, byte-oriented
    values via CacheCodec), but that's a follow-up.
  • No changes to the CacheBackend trait itself.
  • Default behavior (Session::new(index_cache_size, metadata_cache_size, ...))
    is byte-for-byte the same as before.

Usage sketch

Rust:

use lance_core::cache::build_from_uri;
use lance::session::Session;

let index = build_from_uri("moka://?capacity=1073741824")?;
let session = Session::with_cache_backends(
    Some(index), None, /* size defaults */ 0, 0, Default::default(),
);

A hypothetical third-party crate:

// In `my-cache-backend` crate:
pub fn register() -> lance_core::Result<()> {
    lance_core::cache::register_backend("my_backend", build_my_backend)
}

Python:

session = lance.Session(
    index_cache_backend="moka://?capacity=1073741824",
    metadata_cache_backend={"kind": "moka", "options": {"capacity": "268435456"}},
)

Test plan

  • cargo check -p lance-core --tests
  • cargo check -p lance --tests
  • cargo check --manifest-path python/Cargo.toml
  • cargo test -p lance-core --lib cache:: — 39 passed (14 new: 4 registry
    + 10 URI parser + 4 moka via config/URI)
  • cargo test -p lance --lib session::tests — 10 passed (1 new:
    test_with_cache_backends_uses_provided_and_default)
  • Python end-to-end (Session(index_cache_backend="moka://...") roundtrip)
    — reviewers who prefer a wheel-based smoke test, please shout and I'll add
    one.

Discussion link

Design context and open questions:
Discussion #7575 — Proposal: Pluggable cache backends

Summary by CodeRabbit

  • New Features
    • Sessions now accept optional custom cache backends for both index and metadata caches, configurable via URI strings or config maps (including the built-in moka backend with a capacity option).
  • Bug Fixes
    • Added validation for cache configuration, including mutual exclusivity of cache size vs backend, clearer errors for invalid/unknown backend settings, and stricter URI/config parsing rules.
  • Tests
    • Added/updated unit tests covering backend parsing/discovery, invalid inputs, and session cache initialization behavior.

yanghua added 5 commits July 8, 2026 20:18
Add a process-wide registry so cache backends can be discovered by a
stable string identifier (`kind`) instead of being wired into every
call site. Third-party backends integrate by defining a
`BackendBuildFn` and calling `register_backend(kind, build)` once at
application startup; `build_from_config` then locates the constructor
and hands it a `BackendConfig`.

`BackendConfig` is intentionally FFI-friendly (`kind: String`,
`options: HashMap<String, String>`) so the same config shape can be
represented in Python (`dict[str, str]`), Java (`Map<String, String>`),
and — if a future dynamic loader is added — across a stable ABI
without changing the user-visible configuration model.

Duplicate registration under the same `kind` returns an error rather
than silently overriding an existing constructor. This turns "two
crates claim the same backend name" from a mystery cache downgrade
into a startup-time error the operator can fix.

No backend is registered by default; a follow-up commit will
auto-register `MokaCacheBackend` under `"moka"`.
Add `parse_backend_uri` / `build_from_uri` so bindings and config files
can select a cache backend with a compact single-string form
(`moka://?capacity=1073741824`) instead of a typed builder. Bindings
that already exchange strings (Python, Java, YAML config) benefit the
most.

The grammar is a predictable subset of RFC 3986:

    uri     ::= scheme ":" hier ( "?" query )?
    scheme  ::= ALPHA ( ALPHA | DIGIT | "+" | "-" | "." )*
    hier    ::= "//" authority path?   |   path

Mapping to `BackendConfig`:

  * `scheme` (lower-cased) becomes `kind`, matching the registry key.
  * The joined authority + path (with a leading "//" and any leading
    "/" trimmed) is stored under the option `path` when non-empty. If
    the query also sets `path`, the URI is rejected.
  * Each `key=value` pair from the query is percent-decoded into
    `options`. Duplicate keys and pairs without `=` are rejected.

`build_from_uri` composes `parse_backend_uri` with `build_from_config`.
Errors surface the offending URI so misconfiguration is easy to trace.

Ten unit tests cover authority-only URIs, path-and-query URIs,
host-style URIs (`redis://localhost:6379/0`), case-insensitive
schemes, percent-decoding, trailing "&", missing scheme, invalid
scheme, duplicate query keys, and pairs without "=".
Make the Moka backend discoverable through the registry so a bare
Lance installation can build a cache backend from a `BackendConfig` or
URI (`moka://?capacity=...`) without the caller registering it
first. This lets bindings expose a single "cache backend" knob whose
default value ("moka") works everywhere.

Changes:

  * `MokaCacheBackend` now has a `MOKA_BACKEND_KIND` identifier and a
    `build_moka` `BackendBuildFn`. The build fn recognizes a single
    option (`capacity` in bytes), rejects unknown options so typos
    surface at construction time, and defaults to a 0-capacity
    (disabled) cache when `capacity` is missing.
  * `build_from_config` now calls a private `ensure_builtin_backends`
    helper before looking up the requested kind. The helper checks
    whether Moka is already registered and inserts it if not; the
    check is against the current registry contents (rather than a
    process-once flag) so `#[cfg(test)]` helpers that snapshot and
    restore the registry still see the built-in backend after they
    take ownership.
  * Third-party backends are unchanged — they still opt in with their
    own `register()` call, and the registry rejects duplicate `kind`
    registrations.

Four unit tests cover the config path, the URI path, unknown-option
rejection, and capacity-parse errors.
Add `Session::with_cache_backends(index_backend, metadata_backend, ...)`
so callers who have already resolved backend selection through
`lance_core::cache::build_from_config` / `build_from_uri` can inject a
backend for either or both caches in one call. Each argument is
optional; `None` falls back to the same size-driven default that
`Session::new` would have produced.

This closes the asymmetry between the existing `with_index_cache_backend`
(index only) and the metadata cache (previously never receiving a
custom backend at all), and gives language bindings a single Rust
entry point to wire up `index_cache_backend` / `metadata_cache_backend`
parameters.

A unit test verifies the "inject one, default the other" flow using
`build_from_uri("moka://?capacity=...")` end-to-end.
Add `index_cache_backend` and `metadata_cache_backend` keyword arguments
to `lance.Session(...)`. Each accepts either a URI string
(`"moka://?capacity=..."`) that is resolved through
`lance_core::cache::build_from_uri`, or a dict of the form
`{"kind": "moka", "options": {"capacity": "..."}}` that is resolved
through `build_from_config`.

Setting both `*_cache_size_bytes` and `*_cache_backend` for the same
cache raises `ValueError` at binding boundary rather than silently
letting one override the other. Unknown Python types on the backend
kwargs also raise `ValueError` with a clear message pointing at the
offending argument.

Internally the constructor now routes through
`LanceSession::with_cache_backends`, which falls back to the size-based
default when the caller passes `None`, so the previous
`Session(index_cache_size_bytes=..., metadata_cache_size_bytes=...)`
call path is unchanged.
@github-actions github-actions Bot added A-python Python bindings enhancement New feature or request labels Jul 8, 2026
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.13137% with 48 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
rust/lance-core/src/cache/backend_uri.rs 84.61% 17 Missing and 7 partials ⚠️
rust/lance-core/src/cache/registry.rs 86.23% 12 Missing and 7 partials ⚠️
rust/lance-core/src/cache/moka.rs 92.30% 2 Missing and 1 partial ⚠️
rust/lance/src/session.rs 95.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44c853c9-f07a-4214-93e9-a45706e11e43

📥 Commits

Reviewing files that changed from the base of the PR and between 24b4e49 and 40c8e82.

📒 Files selected for processing (1)
  • rust/lance-core/src/cache/registry.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rust/lance-core/src/cache/registry.rs

📝 Walkthrough

Walkthrough

Adds cache-backend configuration and URI parsing in lance-core, wires in the built-in moka backend, adds a Rust Session constructor for custom cache backends, and exposes backend selection through Python bindings, stubs, and tests.

Changes

Configurable cache backends for Lance Session

Layer / File(s) Summary
Backend registry core
rust/lance-core/src/cache/registry.rs
Defines backend configuration and registry APIs, adds built-in backend initialization, and includes registry state helpers and tests for registration, duplicates, reserved kinds, and unknown kinds.
Cache backend URI parsing
rust/lance-core/src/cache/backend_uri.rs
Implements backend URI parsing and construction from compact strings, including scheme validation, path/query handling, percent decoding, and parsing tests.
Moka backend registration and module exports
rust/lance-core/src/cache/moka.rs, rust/lance-core/src/cache/mod.rs
Adds the built-in moka backend builder and exposes registry and URI helpers from the cache module.
Rust Session with_cache_backends constructor
rust/lance/src/session.rs
Adds Session::with_cache_backends, which accepts optional custom cache backends per tier, falls back to capacity-based defaults, and includes a constructor test.
Python Session binding for cache backends
python/src/session.rs
Expands the Python Session binding to accept backend descriptors, resolves URI and dict inputs into cache backends, validates exclusivity with cache sizes, and calls the Rust constructor.
Python stub and tests for Session cache backends
python/python/lance/lance/__init__.pyi, python/python/tests/test_session.py
Adds the typed _Session.__init__ stub for backend parameters and pytest coverage for URI, dict, and invalid cache-backend configurations.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding pluggable cache backend registration and selection support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@yanghua yanghua marked this pull request as ready for review July 9, 2026 07:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
python/python/tests/test_session.py (1)

43-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parametrize the two positive backend-config tests.

test_cache_backend_uri_config and test_cache_backend_dict_config differ only by the index_cache_backend input and share the same assertion, so they should be a single parametrized test.

As per coding guidelines: "use @pytest.mark.parametrize for Python tests when cases differ only by inputs".

♻️ Proposed parametrization
-def test_cache_backend_uri_config():
-    session = lance.Session(index_cache_backend="moka://?capacity=1048576")
-
-    assert session.index_cache_size_bytes() == 0
-
-
-def test_cache_backend_dict_config():
-    session = lance.Session(
-        index_cache_backend={
-            "kind": "moka",
-            "options": {"capacity": "1048576"},
-        },
-    )
-
-    assert session.index_cache_size_bytes() == 0
+@pytest.mark.parametrize(
+    "index_cache_backend",
+    [
+        "moka://?capacity=1048576",
+        {"kind": "moka", "options": {"capacity": "1048576"}},
+    ],
+    ids=["uri", "dict"],
+)
+def test_cache_backend_config(index_cache_backend):
+    session = lance.Session(index_cache_backend=index_cache_backend)
+
+    assert session.index_cache_size_bytes() == 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/python/tests/test_session.py` around lines 43 - 57, Combine
test_cache_backend_uri_config and test_cache_backend_dict_config into one pytest
parametrized test in test_session.py, since they only vary by the
index_cache_backend input and share the same assertion. Use
`@pytest.mark.parametrize` on the test function and pass both existing
backend-config inputs as cases, keeping the assertion on
Session.index_cache_size_bytes() unchanged.

Source: Coding guidelines

rust/lance-core/src/cache/backend_uri.rs (1)

29-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confusing wording in the path-conflict doc note.

"the URI-supplied path wins and the parser errors on the conflict" reads as contradictory — the actual behavior (per parse_backend_uri, Lines 93-98) is that a conflicting path query key always errors, nothing "wins".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/cache/backend_uri.rs` around lines 29 - 34, Update the
doc note in backend_uri so it matches `parse_backend_uri` behavior: the `path`
query key is not overridden by the URI path on conflict, it simply causes an
error. Reword the comment near the authority/path handling to state that the
joined URI path is stored only when no `path` query key exists, and that a
conflicting `path` key results in a parser error rather than one value
“winning.”
rust/lance-core/src/cache/registry.rs (1)

35-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding runnable examples to key public APIs.

BackendConfig::new/with_option (Lines 37-48) and build_from_config (Lines 107-123) are public but lack usage examples; register_backend's doc (Lines 62-76) does include one.

As per coding guidelines, "All public APIs must have documentation with examples, and documentation should link to relevant structs and methods."

Also applies to: 107-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/cache/registry.rs` around lines 35 - 49, Add runnable doc
examples to the public APIs in BackendConfig and registry helpers: document
BackendConfig::new and BackendConfig::with_option with a usage example, and add
an example to build_from_config as well. Keep the examples consistent with the
existing register_backend docs, and make sure the docs link to the relevant
types and methods (BackendConfig, register_backend, build_from_config) so the
public API guidance is satisfied.

Source: Coding guidelines

rust/lance/src/session.rs (1)

160-193: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

with_index_cache_backend is now redundant; consider deprecating it.

with_cache_backends (160-193) supersedes with_index_cache_backend (121-133) — both let a caller inject a custom index backend, with with_cache_backends additionally covering the metadata tier. Leaving both as separate public constructors invites drift/confusion, and per coding guidelines, obsolete public constructors should follow a deprecation path rather than remain side-by-side indefinitely. Separately, both are ad-hoc constructor variants rather than with_-style builder methods chained onto Session::new(...), which the guidelines call out to avoid.

As per coding guidelines, "Delete obsolete internal (pub(crate) or private) methods in the same PR that introduces their replacements; for public API methods, follow the deprecation path in root AGENTS.md instead" and "Use with_-prefixed builder methods for optional config ... and do not create separate constructor variants."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/session.rs` around lines 160 - 193, `with_index_cache_backend`
is now superseded by `Session::with_cache_backends`, so update the public API to
follow the deprecation path instead of keeping both constructors active. Add
deprecation guidance on `with_index_cache_backend` and route callers toward
`with_cache_backends`, which already covers the index backend plus metadata
backend. Keep the implementation behavior consistent in `Session` while avoiding
a long-lived redundant constructor variant that can drift from
`with_cache_backends`.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance-core/src/cache/backend_uri.rs`:
- Around line 162-188: The `percent_decode` helper in `backend_uri.rs` is
applying form-style `+` to space decoding, which conflicts with the RFC 3986
subset described by the module docs. Update `percent_decode` to stop treating
`+` as a space so it only performs percent-escape decoding, and keep the
`BackendUri` parsing behavior consistent with the documented grammar. If you
prefer to preserve `+` handling, then explicitly document that exception in the
module’s RFC section so callers know literal `+` must be encoded as `%2B`.

In `@rust/lance-core/src/cache/registry.rs`:
- Around line 99-105: Move the inline imports in cache::registry to file scope:
relocate the super::moka::{MOKA_BACKEND_KIND, build_moka} and
super::moka::MOKA_BACKEND_KIND uses out of builtin_backend and the other
affected function block(s), then keep those functions referring to the
already-imported symbols. Update the top-level use declarations near the rest of
the module imports so builtin_backend and the related registry logic no longer
contain inline use statements.
- Line 88: The registry lock handling in `registry()` and the other affected
registry access points still uses a bare mutex `.unwrap()`, which can panic in
library code if the lock is poisoned. Replace the `registry().lock().unwrap()`
pattern at the identified registry methods with the same poisoned-lock recovery
used in the test helpers, i.e. recover the inner guard instead of panicking, and
apply the fix consistently across the referenced registry functions so all
production lock acquisitions behave safely.

In `@rust/lance/src/session.rs`:
- Around line 409-433: The test in Session::with_cache_backends only checks a
miss on an unrelated key, so it doesn’t prove the injected index backend from
build_from_uri was actually wired in. Update
test_with_cache_backends_uses_provided_and_default to exercise the
session.index_cache with a value inserted through the injected backend and then
read it back (or otherwise assert a behavior unique to the injected cache),
while keeping the metadata_cache fallback check as-is.
- Around line 160-170: The public API doc for Session::with_cache_backends is
missing a usage example, so add a rust,no_run example to the doc comment near
with_cache_backends that demonstrates constructing a cache backend and calling
Session::with_cache_backends, similar to the existing with_spill_store
documentation. Make sure the example references Session and
build_from_uri/build_from_config as appropriate and includes the relevant doc
links so the new API follows the project’s public-documentation guidelines.

---

Nitpick comments:
In `@python/python/tests/test_session.py`:
- Around line 43-57: Combine test_cache_backend_uri_config and
test_cache_backend_dict_config into one pytest parametrized test in
test_session.py, since they only vary by the index_cache_backend input and share
the same assertion. Use `@pytest.mark.parametrize` on the test function and pass
both existing backend-config inputs as cases, keeping the assertion on
Session.index_cache_size_bytes() unchanged.

In `@rust/lance-core/src/cache/backend_uri.rs`:
- Around line 29-34: Update the doc note in backend_uri so it matches
`parse_backend_uri` behavior: the `path` query key is not overridden by the URI
path on conflict, it simply causes an error. Reword the comment near the
authority/path handling to state that the joined URI path is stored only when no
`path` query key exists, and that a conflicting `path` key results in a parser
error rather than one value “winning.”

In `@rust/lance-core/src/cache/registry.rs`:
- Around line 35-49: Add runnable doc examples to the public APIs in
BackendConfig and registry helpers: document BackendConfig::new and
BackendConfig::with_option with a usage example, and add an example to
build_from_config as well. Keep the examples consistent with the existing
register_backend docs, and make sure the docs link to the relevant types and
methods (BackendConfig, register_backend, build_from_config) so the public API
guidance is satisfied.

In `@rust/lance/src/session.rs`:
- Around line 160-193: `with_index_cache_backend` is now superseded by
`Session::with_cache_backends`, so update the public API to follow the
deprecation path instead of keeping both constructors active. Add deprecation
guidance on `with_index_cache_backend` and route callers toward
`with_cache_backends`, which already covers the index backend plus metadata
backend. Keep the implementation behavior consistent in `Session` while avoiding
a long-lived redundant constructor variant that can drift from
`with_cache_backends`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 13eee651-c894-44b3-a7ca-be53c0ca8503

📥 Commits

Reviewing files that changed from the base of the PR and between d581bb9 and 7b2c02f.

📒 Files selected for processing (8)
  • python/python/lance/lance/__init__.pyi
  • python/python/tests/test_session.py
  • python/src/session.rs
  • rust/lance-core/src/cache/backend_uri.rs
  • rust/lance-core/src/cache/mod.rs
  • rust/lance-core/src/cache/moka.rs
  • rust/lance-core/src/cache/registry.rs
  • rust/lance/src/session.rs

Comment thread rust/lance-core/src/cache/backend_uri.rs
Comment thread rust/lance-core/src/cache/registry.rs Outdated
Comment thread rust/lance-core/src/cache/registry.rs
Comment thread rust/lance/src/session.rs
Comment thread rust/lance/src/session.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-python Python bindings enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant