-
Notifications
You must be signed in to change notification settings - Fork 15
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
Add named scoping support without requiring additional functions. #148
Conversation
@lesnik512 While extending the def __init__(
self,
creator: typing.Callable[P, typing.Iterator[T_co] | typing.AsyncIterator[T_co]],
scope: ContextScope | None = None, # temporary solution
*args: P.args,
**kwargs: P.kwargs,
) -> None:
... However, I cannot insert it in between So the only viable solution I can see is modifying the providers.ContextResource(my_func, scope=Scope.APP).with_args(*my_args).with_kwargs(**my_kwargs) Or by making providers.ContextResource(my_func, scope=Scope.APP, args=my_args, kwargs=my_kwargs) This is of course a breaking API change, but it is necessary if we want to continue to pass more options to Providers, also |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lesnik512 These changes make it so that the warning to remove injection only truly gets triggered when literally nothing was injected or overridden. However, this comes at a non-insignificant performance cost because I basically always need to do isinstance
checks.
So the other suggestion would be:
Completely remove the warning from the @inject
decorator and if someone missuses it that is on them...
Okay seems like both of the above are not an option because of limitations of pythons type system ( providers.ContextResource(my_func, scope=Scope.APP).with_spec(*myargs, **my_kwargs) This also allows to me to remove a LOT of |
|
Yes, thought about this one more time and realized that we will have to split the configuration of the provider into two places anyway (whether its Realized that keeping the providers.ContextResource(my_func, *args, **kwargs).with_config(scope=Scope.APP) This way, all the additional provider parameters can go into the |
Still to-do:
|
Alright, so the way I wrote it currently, named scopes do not prevent entering a resource's scope. This, in combination with the fact that a resource only needs to be initialized to be resolved, leads to the following key example: p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP)
await p.async_resolve() # will raise an exception
async with container_context(p, scope=None):
assert get_current_scope() is None
await p.async_resolve() # will resolve I think this behavior is unintuitive, therefore I will add additional guards on entering a resource context that check if the current scope is the named scope.
|
@lesnik512 Ready for review |
I think async with container_context(p, scope="app"): .... |
@vrslev And how would you allow custom scopes? |
|
Well, the CUSTOM = "CUSTOM"
with container_context(scope=CUSTOM): # problem solved But then you still need to import the
So the current approach both acts as an auto-complete and helps to prevent mistakes, since people literally can't use whatever they want and must import a correctly typed value. I guess my point is the way I wrote it encourages "safe" handling of named contexts. And yes now you need to import stuff, but I think that is a worthy tradeoff. The only thing I can suggest is the following (perhaps slightly unrelated): Currently:class ContextScope:
"""A named context scope."""
def __init__(self, name: str) -> None:
"""Initialize a new context scope."""
self._name = name
@property
def name(self) -> str:
"""Get the name of the context scope."""
return self._name
@override
def __eq__(self, other: object) -> bool:
if isinstance(other, ContextScope):
return self.name == other.name
return False
@override
def __repr__(self) -> str:
return f"{self.name!r}"
class ContextScopes:
"""Enumeration of context scopes."""
ANY = ContextScope("ANY") # special scope that can be used in any context
APP = ContextScope("APP") # application scope
REQUEST = ContextScope("REQUEST") # request scope
INJECT = ContextScope("INJECT") # inject scope This implementation has the following downside:
Proposition: Rename |
Nice write-up. Thanks for clarification, I agree now that current implementation is better than my Literal proposal. |
Currently, one can achieve custom scoping in the following way (example with
fastAPI
):As an additional note, using a
lifespan
to initialize aapp
scoped provider using aContextResource
is not possible because context is stored in acontextVar
and each request enters a new async context. Thus, the only option (at least infastAPI
is to use other providers for app-scoped providers.Goal of this pull request:
Proposed scoping API
Extend
DIContextMiddleware
to acceptscope
Allow
@inject
to accept namedscope
Likely in combination with setting the default scope of the
@inject
decorator toScopes.Function
Notes
ContextResources
that reference that named scope.Related Issues
This will also resolve #139 since function-scoped
ContextResources
will fullfill the requierements in that issue.