Skip to content

fix(python): Add missing SuperpositionDataSource implementation for LocalResolutionProvider#966

Open
ayushjain17 wants to merge 1 commit intomainfrom
provider/python
Open

fix(python): Add missing SuperpositionDataSource implementation for LocalResolutionProvider#966
ayushjain17 wants to merge 1 commit intomainfrom
provider/python

Conversation

@ayushjain17
Copy link
Copy Markdown
Collaborator

@ayushjain17 ayushjain17 commented Apr 11, 2026

ChangeLog

  1. Added missing implementation of SuperpositionDataSource for LocalResolutionProvider in python provider
  2. Re-used fetch_filtered_config for fetch_config in the default implementation to avoid unnecessary implementation

Summary by CodeRabbit

  • New Features

    • Added partialApply parameter to experiment filtering methods, enabling partial application filtering support across Kotlin and Python bindings.
    • LocalProvider now implements data source interface with async methods for fetching configurations and experiments.
  • Refactoring

    • Reorganized config fetching architecture to use filtered-fetch as the primary implementation path across file and HTTP data sources.

@ayushjain17 ayushjain17 requested a review from a team as a code owner April 11, 2026 12:15
Copilot AI review requested due to automatic review settings April 11, 2026 12:15
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a6282bb-05eb-4dae-85b8-051f74fb9ec1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request adds a partialApply / partial_apply boolean parameter to experiment filtering across Rust, Kotlin, and Python codebases, and refactors data source implementations to consolidate config fetching by expressing fetch_config() through a default delegation to fetch_filtered_config().

Changes

Cohort / File(s) Summary
Kotlin FFI Bindings
clients/java/bindings/src/main/kotlin/uniffi/superposition_client/superposition_client.kt
Added partialApply: Byte parameter to FFI binding uniffi_superposition_core_fn_method_providercache_filter_experiment, added FfiConverterBoolean for boolean-to-byte conversion, and updated both interface and implementation method signatures for ProviderCacheInterface.filterExperiment() and ProviderCache.filterExperiment() to accept partialApply: kotlin.Boolean.
Python FFI Bindings
clients/python/bindings/superposition_bindings/superposition_client.py
Updated UniFFI checksum and ctypes FFI signature for uniffi_superposition_core_fn_method_providercache_filter_experiment to accept ctypes.c_int8 argument, added _UniffiConverterBool for boolean conversion, and extended ProviderCacheProtocol.filter_experiment() and ProviderCache.filter_experiment() signatures with partial_apply: bool parameter.
Python Data Sources Refactoring
clients/python/provider/superposition_provider/data_source.py, file_data_source.py, http_data_source.py
Consolidated config fetching: SuperpositionDataSource.fetch_config() now delegates to fetch_filtered_config(). Removed public fetch_config() methods from FileDataSource and HttpDataSource, renaming internal helpers to fetch_filtered_config() as the primary public method with explicit context and prefix_filter parameters.
Python LocalResolutionProvider
clients/python/provider/superposition_provider/local_provider.py
Extended LocalResolutionProvider to implement SuperpositionDataSource with new async fetch methods (fetch_filtered_config, fetch_active_experiments, fetch_candidate_active_experiments, fetch_matching_active_experiments), supports_experiments(), and close(), all serving data from in-memory/FFI caches.
Rust Core Experiment Filtering
crates/superposition_core/src/ffi.rs
Updated ProviderCache::filter_experiment to accept partial_apply: bool parameter, dynamically selecting between partial-apply filtering functions (filter_experiments_by_context, FfiExperimentGroup::filter_by_eval) or standard filtering (get_satisfied_experiments, FfiExperimentGroup::get_satisfied).
Rust Data Source Trait & Implementations
crates/superposition_provider/src/data_source.rs, data_source/file.rs, data_source/http.rs, local_provider.rs
Refactored SuperpositionDataSource trait: fetch_config() now has default implementation delegating to fetch_filtered_config(). Removed fetch_config() from all data source implementations (File, Http, Local), consolidating to single fetch_filtered_config() method that accepts context and prefix_filter parameters. Changed LocalResolutionProvider::init() and close_provider() from public to private.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

bug, quick win

Suggested reviewers

  • mahatoankitkumar
  • sauraww

Poem

🐰 A hop, a skip, and refactoring neat,
Config fetching finds its treat!
Partial apply spreads far and wide,
Through bindings, Rust, and Python's tide.
Data sources dance in sweet reprise,
Default implementations harmonize! 🎵

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the primary focus of the pull request: adding the missing SuperpositionDataSource implementation for LocalResolutionProvider in Python.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch provider/python

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aligns the provider/data-source abstraction across Rust + Python by making fetch_config a default wrapper over fetch_filtered_config, and adds the missing SuperpositionDataSource implementation for the Python LocalResolutionProvider. It also updates UniFFI experiment filtering to support a new partial_apply boolean flag (with regenerated Python/Java bindings).

Changes:

  • Rust: Provide a default SuperpositionDataSource::fetch_config() implementation delegating to fetch_filtered_config(), and update concrete data sources/providers accordingly.
  • Python: Implement SuperpositionDataSource on LocalResolutionProvider and rely on the base-class default fetch_config().
  • UniFFI/bindings: Extend ProviderCache.filter_experiment(...) with a partial_apply parameter and regenerate Python/Java bindings.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
crates/superposition_provider/src/local_provider.rs Refactors lifecycle method visibility and updates data-source trait impl to use fetch_filtered_config.
crates/superposition_provider/src/data_source/http.rs Moves config fetching into fetch_filtered_config under the trait impl; relies on default fetch_config.
crates/superposition_provider/src/data_source/file.rs Implements filtering directly in fetch_filtered_config; relies on default fetch_config.
crates/superposition_provider/src/data_source.rs Adds default fetch_config() delegating to fetch_filtered_config().
crates/superposition_core/src/ffi.rs Adds partial_apply behavior to experiment filtering in ProviderCache.
clients/python/provider/superposition_provider/local_provider.py Adds missing SuperpositionDataSource methods on LocalResolutionProvider.
clients/python/provider/superposition_provider/http_data_source.py Removes fetch_config override in favor of default; keeps fetch_filtered_config.
clients/python/provider/superposition_provider/file_data_source.py Removes fetch_config override in favor of default; keeps fetch_filtered_config.
clients/python/provider/superposition_provider/data_source.py Implements default fetch_config() by calling fetch_filtered_config().
clients/python/bindings/superposition_bindings/superposition_client.py Regenerated UniFFI Python binding for filter_experiment(partial_apply).
clients/java/bindings/src/main/kotlin/uniffi/superposition_client/superposition_client.kt Regenerated UniFFI Kotlin binding for filterExperiment(partialApply).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

pub async fn init(&self, context: EvaluationContext) -> Result<()> {
async fn init(&self, context: EvaluationContext) -> Result<()> {
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

init was changed from pub async fn to a private async fn, which is a breaking API change: current crate examples (e.g. crates/superposition_provider/examples/local_file_example.rs) call provider.init(...) and will no longer compile. Either keep init public (or provide a public wrapper) or update the examples/public API to use the OpenFeature FeatureProvider::initialize entrypoint instead (and document the new usage).

Suggested change
async fn init(&self, context: EvaluationContext) -> Result<()> {
pub async fn init(&self, context: EvaluationContext) -> Result<()> {

Copilot uses AI. Check for mistakes.
}

pub async fn close_provider(&self) -> Result<()> {
async fn close_provider(&self) -> Result<()> {
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

close_provider was made private, but it’s still used externally by the crate examples (e.g. provider.close_provider().await in examples/local_http_example.rs). This is a breaking change that will break cargo test --examples / cargo build --examples. Either keep it public (or expose a public close/shutdown method on the provider) or update the examples and any public docs accordingly.

Suggested change
async fn close_provider(&self) -> Result<()> {
pub async fn close_provider(&self) -> Result<()> {

Copilot uses AI. Check for mistakes.
Comment on lines 80 to 94
@abstractmethod
async def fetch_config(
self,
if_modified_since: Optional[datetime] = None,
) -> FetchResponse[ConfigData]:
"""Fetch the full resolved configuration.

Args:
if_modified_since: Optional timestamp for 304 Not Modified check.

Returns:
FetchResponse with ConfigData or NotModified status.
"""
pass
return await self.fetch_filtered_config(if_modified_since=if_modified_since)

Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

fetch_config still has the @abstractmethod decorator even though it now provides a default implementation. In Python, that means subclasses must override it to be instantiable, so removing fetch_config from HttpDataSource/FileDataSource and adding SuperpositionDataSource to LocalResolutionProvider will make those classes abstract at runtime. Drop @abstractmethod from fetch_config (keep it as a concrete default) so implementors only need to supply fetch_filtered_config.

Copilot uses AI. Check for mistakes.
if_modified_since: Optional[datetime] = None
) -> FetchResponse[ConfigData]:
if_modified_since: Optional[datetime] = None,
) -> FetchResponse:
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

Return type annotation for fetch_filtered_config was loosened to FetchResponse (no generic). This loses typing for callers and is inconsistent with the interface (FetchResponse[ConfigData]). Update the annotation to FetchResponse[ConfigData].

Suggested change
) -> FetchResponse:
) -> FetchResponse[ConfigData]:

Copilot uses AI. Check for mistakes.
for production configurations that need filtering.

Args:
context: Optional context for filtering (ignored).
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

Docstring says context is “ignored”, but this method passes context into _parse_config_file(...) (and the helper JSON-encodes it for the FFI call). Update the docstring to reflect that context is actually applied during parsing/filtering.

Suggested change
context: Optional context for filtering (ignored).
context: Optional context applied during parsing/filtering.

Copilot uses AI. Check for mistakes.
Comment on lines +556 to +565
if not self.ffi_cache or not self.cached_config:
raise RuntimeError("Provider not properly initialized or no config available")

if if_modified_since is not None:
logger.debug("LocalResolutionProvider: ignoring if_modified_since, always reading fresh from file")

return FetchResponse.data(ConfigData(
data=self.ffi_cache.filter_config(context, prefix_filter),
fetched_at=self.cached_config.fetched_at,
))
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

ProviderCache.filter_config expects dimension_data: Optional[dict[str, str]] where values are strings (the UniFFI converter enforces this). This implementation passes context: Dict[str, Any] directly, which will raise at runtime if any values aren’t strings. Convert context to the expected {k: json.dumps(v)} (same as _merge_contexts) before calling into ffi_cache, and update the doc/log message (“reading fresh from file”) since this is filtering cached in-memory data.

Copilot uses AI. Check for mistakes.
Comment on lines +603 to +609
if if_modified_since is not None:
logger.debug("LocalResolutionProvider: ignoring if_modified_since for experiments, always returning cached data")

return FetchResponse.data(ExperimentData(
data=self.ffi_cache.filter_experiment(context, prefix_filter, False),
fetched_at=self.cached_experiments.fetched_at,
))
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

ProviderCache.filter_experiment (UniFFI) also requires dimension_data values to be strings. These calls pass context: Dict[str, Any] directly, which can fail at runtime for non-string values. Convert context to {k: json.dumps(v)} (or reuse the same normalization helper used elsewhere) before calling ffi_cache.filter_experiment(...).

Copilot uses AI. Check for mistakes.
Comment on lines +621 to +630
if not self.ffi_cache or not self.cached_experiments:
raise RuntimeError("Provider not properly initialized or no experiments available")

if if_modified_since is not None:
logger.debug("LocalResolutionProvider: ignoring if_modified_since for experiments, always returning cached data")

return FetchResponse.data(ExperimentData(
data=self.ffi_cache.filter_experiment(context, prefix_filter, True),
fetched_at=self.cached_experiments.fetched_at,
))
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

Same issue as above: context is passed directly into UniFFI filter_experiment but must be dict[str, str] (string values). Convert/normalize the context values before calling into ffi_cache.filter_experiment(...).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clients/python/provider/superposition_provider/http_data_source.py`:
- Around line 65-70: Change the return annotation of the fetch_filtered_config
method to include the generic payload type so it matches the base class
contract: update the signature of fetch_filtered_config to return
FetchResponse[ConfigData] instead of bare FetchResponse; keep the existing
implementation (which constructs ConfigData and uses not_modified()), only
adjust the type hint to FetchResponse[ConfigData] to restore static typing
consistency with other methods like fetch_active_experiments.

In `@clients/python/provider/superposition_provider/local_provider.py`:
- Around line 567-588: fetch_active_experiments currently returns the provider's
internal mutable cache by calling FetchResponse.data(self.cached_experiments);
change this to return a detached copy to avoid exposing internal state. Update
fetch_active_experiments to create and return a deep copy of
self.cached_experiments (e.g., via copy.deepcopy or by constructing new
ExperimentData objects) before passing it to FetchResponse.data, ensuring
callers cannot mutate the provider's cached_experiments; reference the
fetch_active_experiments method, FetchResponse.data, and self.cached_experiments
when making the change.

In `@crates/superposition_provider/src/local_provider.rs`:
- Line 64: The init and close_provider methods were made private but are used by
external examples (e.g., provider.init(...) and provider.close_provider(...)),
causing a breaking change; restore their public visibility by changing their
signatures back to public (e.g., make async fn init(...) -> Result<()> into pub
async fn init(...)->Result<()> and make close_provider public as pub fn
close_provider(...) or equivalent), and ensure any other occurrences (the other
method at the second location) are updated the same way so external callers
compile.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25bd794e-ee60-4e1d-ab1b-74438e47066b

📥 Commits

Reviewing files that changed from the base of the PR and between b4c4f60 and 71b1d89.

📒 Files selected for processing (11)
  • clients/java/bindings/src/main/kotlin/uniffi/superposition_client/superposition_client.kt
  • clients/python/bindings/superposition_bindings/superposition_client.py
  • clients/python/provider/superposition_provider/data_source.py
  • clients/python/provider/superposition_provider/file_data_source.py
  • clients/python/provider/superposition_provider/http_data_source.py
  • clients/python/provider/superposition_provider/local_provider.py
  • crates/superposition_core/src/ffi.rs
  • crates/superposition_provider/src/data_source.rs
  • crates/superposition_provider/src/data_source/file.rs
  • crates/superposition_provider/src/data_source/http.rs
  • crates/superposition_provider/src/local_provider.rs

Comment on lines +65 to +70
async def fetch_filtered_config(
self,
context: Optional[Dict[str, Any]] = None,
prefix_filter: Optional[List[str]] = None,
if_modified_since: Optional[datetime] = None
) -> FetchResponse[ConfigData]:
if_modified_since: Optional[datetime] = None,
) -> FetchResponse:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find clients/python/provider/superposition_provider -name "*.py" -type f | head -20

Repository: juspay/superposition

Length of output: 880


🏁 Script executed:

cat -n clients/python/provider/superposition_provider/http_data_source.py | sed -n '60,85p'

Repository: juspay/superposition

Length of output: 1209


🏁 Script executed:

cat -n clients/python/provider/superposition_provider/data_source.py | head -100

Repository: juspay/superposition

Length of output: 3896


🏁 Script executed:

cat -n clients/python/provider/superposition_provider/data_source.py | sed -n '96,110p'

Repository: juspay/superposition

Length of output: 782


🏁 Script executed:

cat -n clients/python/provider/superposition_provider/http_data_source.py | sed -n '1,150p'

Repository: juspay/superposition

Length of output: 6702


Add the generic type parameter to match the base class contract.

The base class specifies FetchResponse[ConfigData], but this override uses bare FetchResponse at line 70. This drops the payload type annotation and weakens static type checking. The implementation correctly returns ConfigData instances (line 92) and handles not_modified() (line 98), but the return annotation should be -> FetchResponse[ConfigData] to maintain consistency with the abstract contract and match the pattern used by other methods like fetch_active_experiments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/python/provider/superposition_provider/http_data_source.py` around
lines 65 - 70, Change the return annotation of the fetch_filtered_config method
to include the generic payload type so it matches the base class contract:
update the signature of fetch_filtered_config to return
FetchResponse[ConfigData] instead of bare FetchResponse; keep the existing
implementation (which constructs ConfigData and uses not_modified()), only
adjust the type hint to FetchResponse[ConfigData] to restore static typing
consistency with other methods like fetch_active_experiments.

Comment on lines +567 to +588
async def fetch_active_experiments(
self,
if_modified_since: Optional[datetime] = None,
) -> FetchResponse[ExperimentData]:
"""Fetch experiments from file.

Args:
if_modified_since: Timestamp for 304 Not Modified check.

Returns:
FetchResponse with ExperimentData or NotModified status.
"""
if not self.supports_experiments():
raise NotImplementedError("Experiments not supported by this provider")

if not self.cached_experiments:
raise RuntimeError("Provider not properly initialized or no experiments available")

if if_modified_since is not None:
logger.debug("LocalResolutionProvider: ignoring if_modified_since for experiments, always returning cached data")

return FetchResponse.data(self.cached_experiments)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don't expose the cached experiment object by reference.

FetchResponse.data(self.cached_experiments) returns the provider's mutable cache object directly. If a caller mutates that ExperimentData, it rewrites internal state and can affect later evaluations. Return a detached copy here instead of the cached instance itself.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clients/python/provider/superposition_provider/local_provider.py` around
lines 567 - 588, fetch_active_experiments currently returns the provider's
internal mutable cache by calling FetchResponse.data(self.cached_experiments);
change this to return a detached copy to avoid exposing internal state. Update
fetch_active_experiments to create and return a deep copy of
self.cached_experiments (e.g., via copy.deepcopy or by constructing new
ExperimentData objects) before passing it to FetchResponse.data, ensuring
callers cannot mutate the provider's cached_experiments; reference the
fetch_active_experiments method, FetchResponse.data, and self.cached_experiments
when making the change.

}

pub async fn init(&self, context: EvaluationContext) -> Result<()> {
async fn init(&self, context: EvaluationContext) -> Result<()> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep these methods public or update the external API surface.

examples/local_file_example.rs and examples/local_http_example.rs still call provider.init(...) and provider.close_provider(...) directly. Making them private turns this into a breaking change and will make those callers stop compiling.

Suggested fix
-    async fn init(&self, context: EvaluationContext) -> Result<()> {
+    pub async fn init(&self, context: EvaluationContext) -> Result<()> {
-    async fn close_provider(&self) -> Result<()> {
+    pub async fn close_provider(&self) -> Result<()> {

Also applies to: 212-212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/superposition_provider/src/local_provider.rs` at line 64, The init and
close_provider methods were made private but are used by external examples
(e.g., provider.init(...) and provider.close_provider(...)), causing a breaking
change; restore their public visibility by changing their signatures back to
public (e.g., make async fn init(...) -> Result<()> into pub async fn
init(...)->Result<()> and make close_provider public as pub fn
close_provider(...) or equivalent), and ensure any other occurrences (the other
method at the second location) are updated the same way so external callers
compile.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants