diff --git a/rodi/docs/async.md b/rodi/docs/async.md index f124d78..8fe8849 100644 --- a/rodi/docs/async.md +++ b/rodi/docs/async.md @@ -4,16 +4,17 @@ Support for async resolution is intentionally out of the scope of the library be constructing objects should be lightweight. This page provides guidelines for working with objects that require asynchronous -initialization. +initialization or disposal. ## A common example -A common example of this situation are objects that handle TCP/IP connection pooling, -such as `HTTP` clients and database clients. These objects are usually implemented as -*context managers* in Python because they need to implement connection pooling and -gracefully close TCP connections when disposed. +A common example of objects requiring asynchronous disposal are objects that +handle TCP/IP connection pooling, such as `HTTP` clients and database clients. +These objects are typically implemented as *context managers* in Python because +they need to manage connection pooling and gracefully close TCP connections +upon disposal. -Python supports [`asynchronous` context managers](https://peps.python.org/pep-0492/#asynchronous-context-managers-and-async-with) for this kind of scenario. +Python provides [`asynchronous` context managers](https://peps.python.org/pep-0492/#asynchronous-context-managers-and-async-with) for this kind of scenario. Consider the following example, of a `SendGrid` API client to send emails using the SendGrid API, with asynchronous code and using [`httpx`](https://www.python-httpx.org/async/). @@ -82,7 +83,7 @@ class SendGridClient(EmailHandler): }, json=self.get_body(email), ) - # Note: in case of error, inspect response.text + # TODO: in case of error, log response.text response.raise_for_status() # Raise an error for bad responses def get_body(self, email: Email) -> dict: @@ -110,7 +111,7 @@ the one shown on this page to send emails using SendGrid in async code. /// The **SendGridClient** depends on an instance of `SendGridClientSettings` (providing a -SendGrid API Key), and on an instance of `httpx.AsyncClient` able to make HTTP requests. +SendGrid API Key), and on an instance of `httpx.AsyncClient` to make async HTTP requests. The code below shows how to register the object that requires asynchronous initialization and use it across the lifetime of your application. @@ -182,9 +183,10 @@ HTTP client disposed ## Considerations -- It is not Rodi's responsibility to administer the lifecycle of the application. It is - the responsibility of the code that bootstrap the application, to handle objects that - require asynchronous initialization and disposal. +- It is not Rodi's responsibility to administer the lifecycle of the + application. It is the responsibility of the code that bootstraps the + application, to handle objects that require asynchronous initialization and + disposal. - Python's `asynccontextmanager` is convenient for these scenarios. - In the example above, the HTTP Client is configured as singleton to benefit from TCP connection pooling. It would also be possible to configure it as transient or scoped diff --git a/rodi/docs/dependency-inversion.md b/rodi/docs/dependency-inversion.md index 7304945..54781a8 100644 --- a/rodi/docs/dependency-inversion.md +++ b/rodi/docs/dependency-inversion.md @@ -309,9 +309,9 @@ The above prints to screen: type: warning Note how the generics `Repository[Product]` and `Repository[Customer]` are both -configured to be resolved using `Repository` as concrete type. Instances of -`GenericAlias` are not considered as actual classes. The following wouldn't -work: +configured to be resolved using `Repository` as concrete type. In Python, +instances of `GenericAlias` are not considered as actual classes. The following +wouldn't work: ```python container.add_scoped(Repository[Product]) # No. 💥 @@ -375,7 +375,7 @@ key `Repository[T]` when instantiating the `ProductsService`, not for container.add_scoped(Repository[Product], Repository) # No. 💥 ``` -Note that, in practice, this does not cause any issues at runtime, because of +Note that, in practice, this does not cause issues at runtime, because of **type erasure**. For more information, refer to [_Instantiating generic classes and type erasure_](https://typing.python.org/en/latest/spec/generics.html#instantiating-generic-classes-and-type-erasure). If you need to define a more specialized class for `Repository[Product]`, diff --git a/rodi/docs/registering-types.md b/rodi/docs/registering-types.md index f2e1c6d..6488f73 100644 --- a/rodi/docs/registering-types.md +++ b/rodi/docs/registering-types.md @@ -279,6 +279,60 @@ with scoped lifetime: assert c1.context is not c2.context ``` +/// details | Nested scopes. + type: warning + +Rodi was not designed having _nested_ scopes in mind. Scopes are designed to +identify a resolution call for a single event, such as DI resolution for a +single HTTP request. + +Since version `2.0.7`, Rodi offers the possibility to specify the +`ActivationScope` class used by the container, when instantiating the +`Container` object. This class will be used when creating new scopes. Version +`2.0.7` also added an **experimental** class, `TrackingActivationScope` to +support nested scopes transparently, using `contextvars.ContextVar`. + +```python {linenums="1" hl_lines="2 12 16 27"} +def test_nested_scope_1(): + container = Container(scope_cls=TrackingActivationScope) + container.add_scoped(Ok) + provider = container.build_provider() + + with provider.create_scope() as context_1: + a = provider.get(Ok, context_1) + + with provider.create_scope() as context_2: + b = provider.get(Ok, context_2) + + assert a is b + + +def test_nested_scope_2(): + container = Container(scope_cls=TrackingActivationScope) + container.add_scoped(Ok) + provider = container.build_provider() + + with provider.create_scope(): + with provider.create_scope() as context: + a = provider.get(Ok, context) + + with provider.create_scope() as context: + b = provider.get(Ok, context) + + assert a is not b +``` + +/// + +Note how the generics `Repository[Product]` and `Repository[Customer]` are both +configured to be resolved using `Repository` as concrete type. Instances of +`GenericAlias` are not considered as actual classes. The following wouldn't +work: + +```python +container.add_scoped(Repository[Product]) # No. 💥 +container.add_scoped(Repository[Customer]) # No. 💥 +``` ## Using factories diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml index 2d8bf35..445bc93 100644 --- a/rodi/mkdocs.yml +++ b/rodi/mkdocs.yml @@ -34,7 +34,6 @@ theme: name: Switch to dark mode name: "material" custom_dir: overrides/ - # highlightjs: true # ? favicon: img/neoteroi.ico logo: img/neoteroi-w.svg icon: @@ -55,9 +54,6 @@ plugins: - neoteroi.contribs markdown_extensions: - # - markdown.extensions.codehilite: - # linenums: true - # guess_lang: false - pymdownx.highlight: use_pygments: true guess_lang: false