Scopes
Scopes are used to help further manage the lifetime of dependencies in your applications.
Note
Scopes are useful for systems like webservers or worker queues, where certain objects have different lifetime rules. For simple command line applications, these concepts can be ignored.
When setting bindings, there is an option to set the scope as either application
or request scoped. By default, dependencies are set to request scoped.
- Application Scope:Dependencies are created once per application and reused for all requests.
- Request Scope:Dependencies are created newly for each request that is made.Within a single request, objects will still be reused.
When resolving objects with the graph, both scopes will always exist. Scopes are constructed for you if not manually created. For example, the following:
async def main():
graph = FlexGraph()
res = await graph.resolve(create_foo)
Is equivalent to:
async def main():
graph = FlexGraph()
async with graph.application_scope() as app_scope:
async with app_scope.request_scope() as req_scope:
res = await req_scope.resolve(create_foo)
Example Setup
We’ll start by making some basic classes that we can bind to the graph in various ways:
class Foo:
pass
class Bar:
pass
With providers:
def create_foo() -> Iterator[Foo]:
print("Starting Foo")
yield Foo()
print("Ending Foo")
def create_bar() -> Iterator[Bar]:
print("Starting Bar")
yield Bar()
print("Ending Bar")
Using the graph directly
We can use the FlexGraph directly without the entrypoint directive.
The graph supports invoking any callable which has dependencies registered
by the graph. When the graph is called directly, it does not persist any
objects between invocations.
async def multiple_resolves() -> None:
graph = FlexGraph()
print("Example Start")
foo1 = await graph.resolve(create_foo)
foo2 = await graph.resolve(create_foo)
print("Foo1 is Foo2:", foo1 is foo2)
print("Example End")
Example Start
Starting Foo
Ending Foo
Starting Foo
Ending Foo
Foo1 is Foo2: False
Example End
We can see that create_foo is called twice, and that foo is shut down
as soon as we get the result back. This is because each scope is newly created
and destroyed to processes each request.
If we want to re-use results throughout multiple calls, we need to use scopes!
Request Scope
In our previous example, if we want create_foo to be called only once,
then we could do the following:
async def request_scoped_resolve() -> None:
graph = FlexGraph()
print("Before App Scope")
async with graph.application_scope() as app_scope:
print("In App Scope")
print("Before Req Scope")
async with app_scope.request_scope() as req_scope:
print("In Req Scope")
foo1 = await req_scope.resolve(create_foo)
foo2 = await req_scope.resolve(create_foo)
print("Foo1 is Foo2:", foo1 is foo2)
print("After Req Scope")
print("After App Scope")
Before App Scope
In App Scope
Before Req Scope
In Req Scope
Starting Foo
Foo1 is Foo2: True
Ending Foo
After Req Scope
After App Scope
Because we re-used the request scope for multiple calls, we re-used the instance. You’ll also notice that the shutdown of the resource happens only after we close the request scope.
Application Scope
We could also choose to make create_foo set as application scoped:
async def application_scoped_resolve() -> None:
graph = FlexGraph()
graph.bind(create_foo, scope="application")
print("Before App Scope")
async with graph.application_scope() as app_scope:
print("In App Scope")
foo1 = await app_scope.resolve(create_foo)
foo2 = await app_scope.resolve(create_foo)
print("Foo1 is Foo2:", foo1 is foo2)
print("After App Scope")
Before App Scope
In App Scope
Starting Foo
Foo1 is Foo2: True
Ending Foo
After App Scope
Similar to last time, because the application scope was re-used, we re-used the instance. However, this time the shutdown of the resource happens only after we close the application scope.
Eager Dependencies
By default, dependencies are created lazily when requested for the first time. If you want a value to be created as soon as the scope is opened, then you can set the dependency as eager.
The following example illustrates how eager dependencies interact with the different scopes:
async def eager_dependencies() -> None:
graph = FlexGraph()
graph.bind(create_foo, scope="application", eager=True)
graph.bind(create_bar, scope="request", eager=True)
print("Before App Scope")
async with graph.application_scope() as app_scope:
print("In App Scope")
print("Before Req Scope")
async with app_scope.request_scope() as req_scope:
print("In Req Scope")
print("After Req Scope")
print("After App Scope")
Before App Scope
Starting Foo
In App Scope
Before Req Scope
Starting Bar
In Req Scope
Ending Bar
After Req Scope
Ending Foo
After App Scope
Even though we didn’t specifically call create_foo or create_bar,
they were opened when entering the scope they were associated with.
We also notice that they are closed only after their associated scope has
also been closed.