You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I am using this library in my personal development project with FastAPI + SQLAlchemy. For instance, when creating an application that handles a database, I often find the need for a SessionScope to manage instances on a per-session basis.
Here are the requirements I have for SessionScope:
It should be able to hold providers managed by a unique ID such as session_id.
It should allow specifying session_id during dependency resolution.
I attempted to implement SessionScope by inheriting from the Scope class, following the example in docs/scopes.rst. However, I found that inheriting from the Scope class alone doesn't allow managing with session_id effectively.
I believe this is structurally challenging in Injector, Provider, and Scope classes to propagate a unique ID like session_id (or scope_id). If I'm wrong, I'd like your opinion.
So, I propose the following solutions:
example)
Allow Injector to accept session_id (or another unique ID) as an argument.
classInjector:
@synchronized(lock)defget(
self,
interface: Type[T],
scope: Union[ScopeDecorator, Type[Scope], None] =None,
+session_id: Optional[str] =None,
) ->T:
"""_summary_ Args: interface (Type[T]): interface scope (Union[ScopeDecorator, Type[Scope], None], optional): scope. Defaults to None.+ session_id (Optional[str], optional): session_id. Defaults to None. Returns: T: _description_ """binding, binder=self.binder.get_binding(interface)
scope=scopeorbinding.scopeifisinstance(scope, ScopeDecorator):
scope=scope.scope# Fetch the corresponding Scope instance from the Binder.scope_binding, _=binder.get_binding(scope)
scope_instance=scope_binding.provider.get(self)
log.debug("%sInjector.get(%r, scope=%r) using %r", self._log_prefix, interface, scope, binding.provider)
# For request scope, include scopeID as an argument+ifisinstance(scope_instance, SessionScope):
+provider_instance=scope_instance.get(interface, binding.provider, session_id)
else:
provider_instance=scope_instance.get(interface, binding.provider)
+result=provider_instance.get(self, session_id=session_id)
log.debug("%s -> %r", self._log_prefix, result)
returnresultdefcreate_child_injector(self, *args: Any, **kwargs: Any) ->'Injector':
kwargs['parent'] =selfreturnInjector(*args, **kwargs)
defcreate_object(self, cls: Type[T], additional_kwargs: Any=None, session_id: Optional[str] =None) ->T:
"""Create a new instance, satisfying any dependencies on cls."""additional_kwargs=additional_kwargsor {}
log.debug("%sCreating %r object with %r", self._log_prefix, cls, additional_kwargs)
try:
instance=cls.__new__(cls)
exceptTypeErrorase:
reraise(
e,
CallError(cls, getattr(cls.__new__, "__func__", cls.__new__), (), {}, e, self._stack),
maximum_frames=2,
)
init=cls.__init__try:
self.call_with_injection(init, self_=instance, kwargs=additional_kwargs, session_id=session_id)
exceptTypeErrorase:
# Mypy says "Cannot access "__init__" directly"init_function=instance.__init__.__func__# type: ignorereraise(e, CallError(instance, init_function, (), additional_kwargs, e, self._stack))
returninstancedefcall_with_injection(
self,
callable: Callable[..., T],
self_: Any=None,
args: Any= (),
kwargs: Any= {},
+session_id: Optional[str] =None,
) ->T:
"""Call a callable and provide it's dependencies if needed. :param self_: Instance of a class callable belongs to if it's a method, None otherwise. :param args: Arguments to pass to callable. :param kwargs: Keyword arguments to pass to callable. :type callable: callable :type args: tuple of objects :type kwargs: dict of string -> object+ :type session_id: session_id -> Optional[str] :return: Value returned by callable. """bindings=get_bindings(callable)
signature=inspect.signature(callable)
full_args=argsifself_isnotNone:
full_args= (self_,) +full_argsbound_arguments=signature.bind_partial(*full_args)
needed=dict((k, v) for (k, v) inbindings.items() ifknotinkwargsandknotinbound_arguments.arguments)
dependencies=self.args_to_inject(
function=callable,
bindings=needed,
owner_key=self_.__class__ifself_isnotNoneelsecallable.__module__,
+session_id=session_id,
)
dependencies.update(kwargs)
try:
returncallable(*full_args, **dependencies)
exceptTypeErrorase:
reraise(e, CallError(self_, callable, args, dependencies, e, self._stack))
# Needed because of a mypy-related issue (<https://github.com/python/mypy/issues/8129>).assertFalse, "unreachable"# pragma: no cover@private@synchronized(lock)defargs_to_inject(
+self, function: Callable, bindings: Dict[str, type], owner_key: object, session_id: Optional[str] =None
) ->Dict[str, Any]:
"""Inject arguments into a function. :param function: The function. :param bindings: Map of argument name to binding key to inject. :param owner_key: A key uniquely identifying the *scope* of this function. For a method this will be the owning class.+ :param session_id: session_id. :returns: Dictionary of resolved arguments. """dependencies= {}
key= (owner_key, function, tuple(sorted(bindings.items())))
defrepr_key(k: Tuple[object, Callable, Tuple[Tuple[str, type], ...]]) ->str:
owner_key, function, bindings=kreturn"%s.%s(injecting %s)"% (tuple(map(_describe, k[:2])) + (dict(k[2]),))
log.debug("%sProviding %r for %r", self._log_prefix, bindings, function)
ifkeyinself._stack:
raiseCircularDependency(
"circular dependency detected: %s -> %s"% (" -> ".join(map(repr_key, self._stack)), repr_key(key))
)
self._stack+= (key,)
try:
forarg, interfaceinbindings.items():
try:
+instance: Any=self.get(interface, session_id=session_id)
exceptUnsatisfiedRequirementase:
ifnote.owner:
e=UnsatisfiedRequirement(owner_key, e.interface)
raiseedependencies[arg] =instancefinally:
self._stack=tuple(self._stack[:-1])
returndependencies
Allow Provider classes and their subclasses to accept session_id (or another unique ID) as an argument.
classProvider(Generic[T]):
"""Provides class instances."""__metaclass__=ABCMeta@abstractmethod+defget(self, injector: 'Injector', session_id: Optional[str] =None) ->T:
raiseNotImplementedError# pragma: no coverclassClassProvider(Provider, Generic[T]):
"""Provides instances from a given class, created using an Injector."""def__init__(self, cls: Type[T]) ->None:
self._cls=cls+defget(self, injector: 'Injector', session_id: Optional[str] =None) ->T:
+returninjector.create_object(self._cls, session_id=session_id)
classCallableProvider(Provider, Generic[T]):
"""Provides something using a callable. The callable is called every time new value is requested from the provider. There's no need to explicitly use :func:`inject` or :data:`Inject` with the callable as it's assumed that, if the callable has annotated parameters, they're meant to be provided automatically. It wouldn't make sense any other way, as there's no mechanism to provide parameters to the callable at a later time, so either they'll be injected or there'll be a `CallError`. :: >>> class MyClass: ... def __init__(self, value: int) -> None: ... self.value = value ... >>> def factory(): ... print('providing') ... return MyClass(42) ... >>> def configure(binder): ... binder.bind(MyClass, to=CallableProvider(factory)) ... >>> injector = Injector(configure) >>> injector.get(MyClass) is injector.get(MyClass) providing providing False """def__init__(self, callable: Callable[..., T]):
self._callable=callable+defget(self, injector: 'Injector', session_id: Optional[str] =None) ->T:
+returninjector.call_with_injection(self._callable, session_id=session_id)
def__repr__(self) ->str:
return'%s(%r)'% (type(self).__name__, self._callable)
classInstanceProvider(Provider, Generic[T]):
"""Provide a specific instance. :: >>> class MyType: ... def __init__(self): ... self.contents = [] >>> def configure(binder): ... binder.bind(MyType, to=InstanceProvider(MyType())) ... >>> injector = Injector(configure) >>> injector.get(MyType) is injector.get(MyType) True >>> injector.get(MyType).contents.append('x') >>> injector.get(MyType).contents ['x'] """def__init__(self, instance: T) ->None:
self._instance=instance+defget(self, injector: 'Injector', session_id: Optional[str] =None) ->T:
returnself._instancedef__repr__(self) ->str:
return'%s(%r)'% (type(self).__name__, self._instance)
@privateclassListOfProviders(Provider, Generic[T]):
"""Provide a list of instances via other Providers."""_providers: List[Provider[T]]
def__init__(self) ->None:
self._providers= []
defappend(self, provider: Provider[T]) ->None:
self._providers.append(provider)
def__repr__(self) ->str:
return'%s(%r)'% (type(self).__name__, self._providers)
classMultiBindProvider(ListOfProviders[List[T]]):
"""Used by :meth:`Binder.multibind` to flatten results of providers that return sequences."""+defget(self, injector: 'Injector', session_id: Optional[str] =None) ->List[T]:
return [iforproviderinself._providersforiinprovider.get(injector)]
classMapBindProvider(ListOfProviders[Dict[str, T]]):
"""A provider for map bindings."""+defget(self, injector: 'Injector', session_id: Optional[str] =None) ->Dict[str, T]:
map: Dict[str, T] = {}
forproviderinself._providers:
map.update(provider.get(injector))
returnmap
Implementation of SessionScope
classSessionScope(Scope):
"""A :class:`Scope` that returns a per-Injector instance for a session_id and a key. :data:`session` can be used as a convenience class decorator. >>> class A: pass >>> injector = Injector() >>> provider = ClassProvider(A) >>> session = SessionScope(injector) >>> a = session.get(A, provider, session_id) >>> b = session.get(A, provider, session_id2) >>> a is b False >>> c = session.get(A, provider, session_id2) >>> b is c True """_context: Dict[str, Dict[type, Provider]]
defconfigure(self) ->None:
self._context= {}
@synchronized(lock)defget(self, key: Type[T], provider: Provider[T], session_id: Optional[str] =None) ->Provider[T]:
id: str=session_idor"common"try:
returnself._context[id][key]
exceptKeyError:
instance=self._get_instance(key, provider, self.injector)
provider=InstanceProvider(instance)
ifidnotinself._context:
self._context[id] = {}
self._context[id][key] =providerreturnproviderdef_get_instance(self, key: Type[T], provider: Provider[T], injector: Injector) ->T:
ifinjector.parentandnotinjector.binder.has_explicit_binding_for(key):
try:
returnself._get_instance_from_parent(key, provider, injector.parent)
except (CallError, UnsatisfiedRequirement):
passreturnprovider.get(injector)
def_get_instance_from_parent(self, key: Type[T], provider: Provider[T], parent: Injector) ->T:
singleton_scope_binding, _=parent.binder.get_binding(type(self))
singleton_scope=singleton_scope_binding.provider.get(parent)
provider=singleton_scope.get(key, provider)
returnprovider.get(parent)
session_scope=ScopeDecorator(SessionScope)
Regarding 3., I don't know how to destroy the _context session at an arbitrary time. Is there a better way to do this as well?
The text was updated successfully, but these errors were encountered:
There's a footgun in the proposed solution, as @tomyou666 points out
Regarding 3., I don't know how to destroy the _context session at an arbitrary time
This will result in a memory leak. Session storage MUST be cleared after a configurable timeout to prevent that. It might be possible to achieve that using TTLCache from cachetools.
That said, I'm not sure if the maintainers of this project are willing to make changes to the API of injector to add an optional session_id argument to accommodate session-scoping of objects. Especially since it should be possible to achieve this with a @provider decorated function in a Module. The solution method that I have in mind would be as follows
Add FastAPI middleware that reads the session-id header/cookie and stores in the context variable.
In your @provider decorated function, read the session-id from the context variable. Use a TTLCache to cache instances where the session-id is used as key.
I am using this library in my personal development project with FastAPI + SQLAlchemy. For instance, when creating an application that handles a database, I often find the need for a SessionScope to manage instances on a per-session basis.
Here are the requirements I have for SessionScope:
I attempted to implement SessionScope by inheriting from the Scope class, following the example in docs/scopes.rst. However, I found that inheriting from the Scope class alone doesn't allow managing with session_id effectively.
I believe this is structurally challenging in Injector, Provider, and Scope classes to propagate a unique ID like session_id (or scope_id). If I'm wrong, I'd like your opinion.
So, I propose the following solutions:
example)
Regarding 3., I don't know how to destroy the
_context
session at an arbitrary time. Is there a better way to do this as well?The text was updated successfully, but these errors were encountered: