Skip to content

Support optional parameters #276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

AvlWx2014
Copy link

@AvlWx2014 AvlWx2014 commented Jul 18, 2025

Overview

Support bindings for optional parameters.

Background

In application code some pieces of configuration can be considered optional. For example, consider an API server that supports authentication/authorization in staging and production, but can have this feature "turned off" for development by simply omitting the auth section of the server configuration.

injector already supports providers for optional types e.g.:

AuthConfig: TypeAlias = OAuth2Config | OidcConfig


@dataclass
class AppConfig:
    auth: AuthConfig | None = None


class ConfigurationModule(Module):
    @singleton
    @provider
    def provide_app_config(self) -> AppConfig:
        return AppConfig.load()

    @singleton
    @provider
    def provide_auth_config_or_none(self, app_config: AppConfig) -> AuthConfig | None:
        return app_config.auth

In this example, I can easily set up an injector and get AuthConfig | None from the graph:

World: Final[Injector] = Injector(ConfigurationModule())

if __name__ == "__main__":
    appconfig = World.get(AppConfig)
    print(f"AppConfig resolved: {appconfig!s}"
    
    authconfig = World.get(AuthConfig | None)
    print(f"AuthConfig | None resolved: {authconfig!s}")

When this is run it prints:

AppConfig resolved: ...   # real values hidden for brevity and security
AuthConfig | None resolved: None

However, if I then write a provider function which requests AuthConfig | None as a dependency like:

class MiddlewareModule(Module):
    @provider
    @singleton
    def provide_authn_middleware(
        self, config: AuthConfig | None, injector: Injector
    ) -> AuthenticationMiddleware[Secret[str] | None]:
        middleware: AuthenticationMiddleware[Secret[str] | None]
        match config:
            case OAuth2Config() | OidcConfig():
                middleware = injector.get(BearerTokenMiddleware)
            case None:
                middleware = NoopAuthenticationMiddleware()
            case _:
                assert_never(config)
        return middleware

The injector raises indicating there is no provider for Union[OAuth2Config, OidcConfig] (type names shortened for brevity). Given the AuthConfig: TypeAlias = OAuth2Config | OidcConfig alias from earlier this message implies something somewhere is requesting plain AuthConfig as a dependency rather than AuthConfig | None, which has a provider in the graph.

However, after combing my whole dependency graph I could not find a place where this was actually happening in my application code so I started digging further. After a lot of debugging I found that the function _infer_injected_bindings removes NoneType from unions before resolving the rest of the injectable dependencies from a function signature:

# We don't treat Optional parameters in any special way at the moment.
union_members = v.__args__
new_members = tuple(set(union_members) - {type(None)})

After removing the set difference operation from the assignment to new_members the code examples I share above start working instead of raising an exception about missing providers.

Solution

The solution proposed here removes - {type(None)} from new_members = ... to allow optional dependencies to be fulfilled if there is a provider configured for them.

I also included a test to demonstrate the behavior in test_get_bindings. In the first commit the new test fails, but in the second commit with the proposed change applied it passes.

This patch induces a change in the test for test_get_bindings_for_pep_604 since now a: int | None resolves to int | None instead of just int.

The rest of the test suite passes with this patch in place, so from what I can tell there aren't any unintended side effects on the rest of the codebase, but let me know if I'm missing something. Looking forward to getting your thoughts on this!

@AvlWx2014
Copy link
Author

@davidparsson @jstasiak wanted to get your thoughts on this. Feel free to tag anyone else for discussion that I may have missed!

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.

1 participant