Skip to content

Conversation

@fadedDexofan
Copy link
Contributor

@fadedDexofan fadedDexofan commented Nov 29, 2025

This is attempt to re-implement same behavior from draft-state PR in dishka reagento/dishka#561

@fadedDexofan fadedDexofan self-assigned this Nov 29, 2025
@sourcery-ai
Copy link

sourcery-ai bot commented Nov 29, 2025

Reviewer's Guide

Introduce a conditional provider activation system into the DI layer, allowing providers to be registered with predicates and filtered at module build time based on an activation context, while keeping default behavior unchanged when no conditions are specified.

Sequence diagram for module build with conditional provider filtering

sequenceDiagram
    actor Dev
    participant WakuFactory
    participant ModuleRegistryBuilder as RegistryBuilder
    participant _ActivationBuilder as Builder
    participant Module
    participant ProviderFilter
    participant ConditionalProvider as CondProv
    participant Provider

    Dev->>WakuFactory: create()
    WakuFactory->>RegistryBuilder: new(root_module_type, context, provider_filter)

    WakuFactory->>RegistryBuilder: build()
    activate RegistryBuilder

    RegistryBuilder->>RegistryBuilder: _collect_modules()
    RegistryBuilder->>Builder: _build_type_registry(modules)
    loop for each ModuleMetadata
        alt ProviderSpec is ConditionalProvider
            RegistryBuilder->>Builder: register(spec.provided_type)
        else ProviderSpec is Provider
            RegistryBuilder->>Builder: register(factory.provides.type_hint)
        end
    end

    RegistryBuilder->>RegistryBuilder: _register_modules(post_order)
    loop for each ModuleMetadata
        RegistryBuilder->>Module: new(module_type, metadata)
        RegistryBuilder->>Module: create_provider(context, Builder, ProviderFilter)
        activate Module
        Module->>ProviderFilter: filter(self.providers, context, module_type, Builder)
        activate ProviderFilter
        loop for each ProviderSpec
            alt spec is ConditionalProvider
                ProviderFilter->>ProviderFilter: build ActivationContext
                ProviderFilter->>CondProv: when(ctx)
                alt activator returns true
                    ProviderFilter->>CondProv: get provider
                    CondProv-->>ProviderFilter: provider
                    ProviderFilter->>Module: include provider
                else activator returns false
                    ProviderFilter->>ProviderFilter: optionally on_skip
                end
            else spec is Provider
                ProviderFilter-->>Module: include provider
            end
        end
        deactivate ProviderFilter

        Module->>Module: _ModuleProvider(active_providers)
        Module-->>RegistryBuilder: BaseProvider
        deactivate Module
        RegistryBuilder->>RegistryBuilder: store provider
    end

    RegistryBuilder-->>WakuFactory: ModuleRegistry
    deactivate RegistryBuilder

    WakuFactory->>Dev: WakuApplication
Loading

Class diagram for conditional provider activation types

classDiagram
    direction LR

    class ActivationBuilder {
        <<protocol>>
        +has_active(type_: Any) bool
    }

    class ActivationContext {
        +dict~Any,Any~ container_context
        +ModuleType module_type
        +DynamicModule module_type
        +Any provided_type
        +ActivationBuilder builder
    }

    class Activator {
        <<typealias>>
    }

    class Has {
        +Any type_
        +__call__(ctx: ActivationContext) bool
    }

    class ConditionalProvider {
        +Provider provider
        +Activator when
        +Any provided_type
    }

    class IProviderFilter {
        <<protocol>>
        +filter(providers: list~ProviderSpec~, context: dict~Any,Any~, module_type: ModuleType, builder: ActivationBuilder) list~Provider~
    }

    class ProviderFilter {
        +OnSkipCallback on_skip
        +filter(providers: list~ProviderSpec~, context: dict~Any,Any~, module_type: ModuleType, builder: ActivationBuilder) list~Provider~
    }

    class OnSkipCallback {
        <<typealias>>
    }

    class ProviderSpec {
        <<typealias>>
    }

    class _ActivationBuilder {
        -set~Any~ _registered_types
        +register(type_: Any) void
        +has_active(type_: Any) bool
    }

    class ModuleMetadata {
        +list~ProviderSpec~ providers
        +list~ModuleType~ imports
        +list~ModuleExtension~ extensions
    }

    class Module {
        -BaseProvider _provider
        +Sequence~ProviderSpec~ providers
        +create_provider(context: dict~Any,Any~, builder: ActivationBuilder, provider_filter: IProviderFilter) BaseProvider
        +provider BaseProvider
    }

    class _ModuleProvider {
        +_ModuleProvider(providers: Iterable~Provider~)
    }

    class ModuleRegistryBuilder {
        -ModuleCompiler _compiler
        -ModuleType _root_module_type
        -dict~Any,Any~ _context
        -IProviderFilter _provider_filter
        -_ActivationBuilder _builder
        +build() ModuleRegistry
        +_build_type_registry(modules: list~tuple~ModuleType,ModuleMetadata~~) void
    }

    class WakuFactory {
        -dict~Any,Any~ _context
        -IProviderFilter _provider_filter
        +create() WakuApplication
    }

    %% Relationships
    Activator --> ActivationContext : takes
    Has --> ActivationContext : uses
    Has ..|> Activator

    ConditionalProvider --> Provider : wraps
    ConditionalProvider --> Activator : when

    ProviderFilter ..|> IProviderFilter
    ProviderFilter --> ConditionalProvider : filters
    ProviderFilter --> ActivationContext : creates

    _ActivationBuilder ..|> ActivationBuilder

    ModuleMetadata --> ProviderSpec : providers
    Module --> ModuleMetadata : constructed_from
    Module --> ProviderSpec : providers
    Module --> BaseProvider : provider
    Module --> IProviderFilter : uses
    Module --> ActivationBuilder : uses

    _ModuleProvider --> Provider : aggregates
    ModuleRegistryBuilder --> Module : builds
    ModuleRegistryBuilder --> _ActivationBuilder : owns
    ModuleRegistryBuilder --> ModuleMetadata : uses
    ModuleRegistryBuilder --> IProviderFilter : uses

    WakuFactory --> ModuleRegistryBuilder : creates
    WakuFactory --> IProviderFilter : configures
    WakuFactory --> ActivationContext : supplies_context
Loading

File-Level Changes

Change Details Files
Add conditional provider API and wiring throughout DI and module system.
  • Introduce ActivationContext, Activator protocol, Has activator, ConditionalProvider wrapper, and ProviderFilter/IProviderFilter strategy plus ActivationBuilder protocol in new activation module and export them via waku.di.init
  • Extend provider factory helpers (singleton, scoped, transient, object, contextual, many) to accept an optional when predicate, returning either a raw Provider or a ConditionalProvider via a ProviderSpec type alias
  • Change ModuleMetadata.providers and Module.providers to store ProviderSpec instead of BaseProvider and update tests and extensions to accept ProviderSpec where needed
src/waku/di/_activation.py
src/waku/di/_providers.py
src/waku/di/__init__.py
src/waku/modules/_metadata.py
tests/data.py
Filter providers per module at registry build time using activation context and registered types.
  • Add an internal _ActivationBuilder in ModuleRegistryBuilder to track all provided types (from both regular and ConditionalProvider specs) and expose has_active for Has activator
  • Extend ModuleRegistryBuilder to accept context and IProviderFilter, build a type registry before registering modules, and for each Module call Module.create_provider with context, builder, and filter to get only active providers
  • Refactor Module to lazily create and cache its aggregated provider via create_provider and a backing _provider field, and adjust _ModuleProvider constructor to accept dishka.Provider instances
src/waku/modules/_registry_builder.py
src/waku/modules/_module.py
Plumb activation context and provider filter from application factory to registry builder.
  • Extend WakuFactory to accept an optional provider_filter and pass both context and provider_filter into ModuleRegistryBuilder
  • Ensure ModuleRegistryBuilder.build uses stored context when filtering providers
src/waku/factory.py
Add tests and examples for conditional activation behavior and provider helpers.
  • Add unit tests for ActivationContext and Has, ConditionalProvider behavior and typing, and integration tests covering activation scenarios including environment flags, missing factories, custom filters, and on-skip callback
  • Add an examples/conditional_providers.py script demonstrating environment-based provider selection and Has-based activation using modules and WakuFactory
tests/di/activation/test_activation_context.py
tests/di/activation/test_conditional_providers.py
tests/di/activation/test_activation_integration.py
examples/conditional_providers.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • In object_, you compute actual_type but still pass the original provided_type (which may be None) into provider(...) while using actual_type only for the ConditionalProvider.provided_type; consider passing actual_type into provider as well so the underlying factory’s provides.type_hint stays consistent with the ConditionalProvider.provided_type and with the non-conditional behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `object_`, you compute `actual_type` but still pass the original `provided_type` (which may be `None`) into `provider(...)` while using `actual_type` only for the `ConditionalProvider.provided_type`; consider passing `actual_type` into `provider` as well so the underlying factory’s `provides.type_hint` stays consistent with the `ConditionalProvider.provided_type` and with the non-conditional behavior.

## Individual Comments

### Comment 1
<location> `src/waku/di/_providers.py:224` </location>
<code_context>
+
+
+@overload
+def contextual(provided_type: Any, *, scope: Scope = Scope.REQUEST) -> Provider: ...
+

</code_context>

<issue_to_address>
**issue (review_instructions):** `contextual` uses `Any` for `provided_type`, which breaks the "avoid Any" guideline and could be modeled with a generic type argument instead.

The new overloads and implementation for `contextual` take `provided_type: Any`. Given the guidelines to avoid `Any` and use generics/aliases, this API should be generic, e.g.:

```python
_T = TypeVar('_T')

@overload
def contextual(provided_type: type[_T], *, scope: Scope = Scope.REQUEST) -> Provider: ...
```

and propagate `_T` through the return type. That keeps the DI surface strongly typed while still flexible.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `*.py`

**Instructions:**
Avoid `Any` type - create proper types instead

</details>
</issue_to_address>

### Comment 2
<location> `tests/di/activation/test_activation_integration.py:21` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 3
<location> `tests/di/activation/test_activation_integration.py:22` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 4
<location> `tests/di/activation/test_conditional_providers.py:18` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Don't import test modules. ([`dont-import-test-modules`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/dont-import-test-modules))

<details><summary>Explanation</summary>Don't import test modules.

Tests should be self-contained and don't depend on each other.

If a helper function is used by multiple tests,
define it in a helper module,
instead of importing one test from the other.
</details>
</issue_to_address>

### Comment 5
<location> `examples/conditional_providers.py:125-126` </location>
<code_context>
    def get_user(self, user_id: str) -> str:
        cached = self.cache.get(f'user:{user_id}')
        if cached:
            return cached
        user_data = f'User-{user_id}'
        self.cache.set(f'user:{user_id}', user_data)
        return user_data

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
        if cached := self.cache.get(f'user:{user_id}'):
```
</issue_to_address>

### Comment 6
<location> `tests/di/activation/test_activation_integration.py:234` </location>
<code_context>
async def test_custom_filter_receives_providers_and_context() -> None:
    received: list[tuple[list[ProviderSpec], dict[Any, Any] | None, ModuleType | DynamicModule, ActivationBuilder]] = []

    class RecordingFilter(IProviderFilter):
        def filter(  # noqa: PLR6301
            self,
            providers: list[ProviderSpec],
            context: dict[Any, Any] | None,
            module_type: ModuleType | DynamicModule,
            builder: ActivationBuilder,
        ) -> list[Provider]:
            received.append((list(providers), context, module_type, builder))
            return [p if isinstance(p, Provider) else p.provider for p in providers]

    AppModule = create_basic_module(
        providers=[scoped(Service)],
        name='AppModule',
    )

    app = WakuFactory(
        AppModule,
        context={'env': 'test'},
        provider_filter=RecordingFilter(),
    ).create()

    async with app, app.container() as container:
        await container.get(Service)

    assert len(received) >= 1
    _providers, ctx, _module_type, _builder = received[0]
    assert ctx is not None
    assert ctx.get('env') == 'test'

</code_context>

<issue_to_address>
**suggestion (code-quality):** Simplify sequence length comparison ([`simplify-len-comparison`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/simplify-len-comparison/))

```suggestion
    assert received
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov
Copy link

codecov bot commented Nov 29, 2025

Codecov Report

❌ Patch coverage is 97.45547% with 10 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
tests/di/activation/test_activation_integration.py 97.74% 3 Missing and 1 partial ⚠️
src/waku/modules/_module.py 76.92% 2 Missing and 1 partial ⚠️
tests/di/activation/test_conditional_providers.py 96.77% 2 Missing ⚠️
src/waku/modules/_registry_builder.py 95.45% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@fadedDexofan fadedDexofan force-pushed the feat/provider-activation branch from 75a5079 to ae6db23 Compare November 29, 2025 08:27
@fadedDexofan fadedDexofan merged commit a9aee1d into master Nov 29, 2025
10 checks passed
@fadedDexofan fadedDexofan deleted the feat/provider-activation branch November 29, 2025 08:31
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