Skip to content
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

Merged
merged 29 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
eee0606
Fix incorrect warnings with @inject decorator when overriding.
alexanderlazarev0 Jan 25, 2025
55dbbea
Removed useless lines.
alexanderlazarev0 Jan 25, 2025
9358492
Temporary implementation
alexanderlazarev0 Jan 26, 2025
6ae5b5e
Migrated to with_spec api.
alexanderlazarev0 Jan 26, 2025
10d21cd
Removed spec api, changed to with_config api.
alexanderlazarev0 Jan 27, 2025
3be73ac
Added named scope entering with container_context.
alexanderlazarev0 Jan 28, 2025
91a10ba
Implemented default scope.
alexanderlazarev0 Jan 28, 2025
5297fd4
Added any scope.
alexanderlazarev0 Jan 29, 2025
1d8b87a
Typo.
alexanderlazarev0 Jan 29, 2025
7bc86b7
Updated inject decorator to accept scope.
alexanderlazarev0 Jan 29, 2025
a10ac99
Implemented scoped injection.
alexanderlazarev0 Jan 29, 2025
00fa2fd
Fixed logical error.
alexanderlazarev0 Jan 29, 2025
8c7dcd7
Made with_config() actually return a new instance.
alexanderlazarev0 Jan 29, 2025
a370bbf
Implemented scoping for DIContextMiddleware.
alexanderlazarev0 Jan 29, 2025
863b149
Added new scopes and made INJECTION default scope for @inject.
alexanderlazarev0 Jan 29, 2025
abf209e
Added strict_scope option and wrote more tests.
alexanderlazarev0 Jan 29, 2025
3bc8ef2
Updated migration guide.
alexanderlazarev0 Jan 29, 2025
770f729
Refactored pre-defined scopes.
alexanderlazarev0 Jan 29, 2025
8958553
Added named scopes documentation.
alexanderlazarev0 Jan 29, 2025
6c0730f
Fixed default scope in migration guide.
alexanderlazarev0 Jan 29, 2025
4b1142d
Fixed working and typo in migration guide.
alexanderlazarev0 Jan 29, 2025
72bf2dd
Added seed to make concurrency tests stable.
alexanderlazarev0 Jan 29, 2025
699ebb9
Reformatted and fixed typos in the named-scopes documentation.
alexanderlazarev0 Jan 30, 2025
fb3c267
Implemented scope checking when entering context.
alexanderlazarev0 Jan 30, 2025
50edb90
Made container_context correctly handle scoped context-resource selec…
alexanderlazarev0 Jan 30, 2025
fd6417a
Adjusted context-resource documentation.
alexanderlazarev0 Jan 30, 2025
024dc73
Enabled force init of context for context-resources.
alexanderlazarev0 Jan 30, 2025
1dc799b
Moved container_context resolution of initial conditions to __enter__.
alexanderlazarev0 Jan 30, 2025
c09b99a
Updated documentation.
alexanderlazarev0 Jan 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# -- Project information
project = "that-depends"
copyright = "2024, Modern Python"
copyright = "2025, Modern Python"
author = "Shiriev Artur"

release = ""
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
introduction/litestar
introduction/faststream
introduction/inject-factories
introduction/scopes
introduction/multiple-containers
introduction/dynamic-container
introduction/application-settings
Expand Down
171 changes: 171 additions & 0 deletions docs/introduction/scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Named Scopes

Named scopes allow you to define the lifecycle of a `ContextResource`.
In essence, they provide a tool to manage when `ContextResources` can be resolved and when they should be finalized.

Before continuing, make sure you're familiar with `ContextResource` providers by reading their [documentation](../providers/context-resources.md).

## Quick Start

By default, `ContextResources` have the named scope `ANY`, meaning they will be re-initialized each time you enter a named scope.
You can change the scope of a `ContextResource` in two ways:

### Setting the scope for providers

1. By setting the `default_scope` attribute in the container class:

```python
class MyContainer(BaseContainer):
default_scope = ContextScope.APP
p = providers.ContextResource(my_resource)
```

2. By calling the `with_config()` method when creating a `ContextResource`. This also overrides the class default:

```python
p = providers.ContextResource(my_resource).with_config(scope=ContextScope.APP)
```

### Entering and exiting scopes

Once you have assigned scopes to providers, you can enter a named scope using `container_context()`.
After entering a scope, you can resolve resources that have been defined with that scope:

```python
from that_depends import container_context

async with container_context(scope=ContextScopes.APP):
# resolve resources with scope APP
await my_app_scoped_provider.async_resolve()
```

## Checking the current scope

If you want to check the current scope, you can use the `get_current_scope()` function:

```python
from that_depends.providers.context_resources import get_current_scope, ContextScopes

async with container_context(scope=ContextScopes.APP):
assert get_current_scope() == ContextScopes.APP
```

## Understanding resolution & strict scope providers

In order for a `ContextResource` to be resolved, you must first initialize the context for that resource.
When you call `container_context(scope=ContextScopes.APP)` this both enters the `APP` scope and (re-)initializes context for
all providers that have `APP` scope. Scoped resources will prevent their context initialization if the current scope does
not match their scope:
```python
p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP)

async with p.async_context():
# will raise an InvalidContextError since current scope is `None`
...
```

Similarly, this will also not work:
```python
async with container_context(p, scope=ContextScopes.REQUEST):
# will raise and InvalidContextError since you are entering `REQUEST` scope
...
```

Once the context has been initialized, a resource can be resolved regardless of the current scope. For example:

```python
await p.async_resolve() # will raise an exception

async with container_context(p, scope=ContextScopes.APP):
val_1 = await p.async_resolve() # will resolve
async with container_context(p, scope=ContextScopes.REQUEST):
val_2 = await p.async_resolve() # will resolve
assert val_1 == val_2 # but value stays the same since context is the same
```

If you want resources to be resolved **only** in the specified scope, enable strict resolution:

```python
p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP, strict_scope=True)
async with container_context(p, scope=ContextScopes.APP):
await p.async_resolve() # will resolve

async with container_context(scope=ContextScopes.REQUEST):
await p.async_resolve() # will raise an exception
```

## Entering a context by force

If you for some reason need to (re-)initialize a context for a `ContextResource` outside of its defined scope,
you can force enter its context:
```python
p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP)

async with p.async_context(force=True):
assert get_current_scope() == None
await p.async_resolve() # will resolve
```
Or similarly using the `context` wrapper (both `ContextResource` providers and containers provide this API):
```python
class Container(BaseContainer):
p = providers.ContextResource(my_resource).with_config(scope=ContextScopes.APP)

@Container.context(force=True)
@inject
async def injected(val = Provide[Container.p]):
return p

await injected() # will resolve
```

## Predefined scopes

`that-depends` includes four predefined scopes in the `ContextScopes` class:

- `ANY`: Indicates that a resource can be resolved in any scope (even `None`). This scope cannot be entered, so it won’t be accepted by any class or method that requires entering a named scope.

- `APP`: A convenience scope with no special behavior.

- `REQUEST`: A convenience scope with no special behavior.

- `INJECT`: The default scope of the `@inject` wrapper. Read more in the [Named scopes with the @inject wrapper](#named-scopes-with-the-inject-wrapper) section.

> **Note:** The default scope, before entering any named scope, is `None`. You can pass `None` as a scope to providers, but since it cannot be entered, in most scenarios passing `None` simply means you did not specify a scope.

## Named scopes with the `@inject` wrapper

The `@inject` wrapper also supports named scopes. Its default scope is `INJECT`, but you can pass any scope you like:

```python
@inject(scope=ContextScopes.APP)
def foo(...):
get_current_scope() # APP
```

When you pass a scope to the `@inject` wrapper, it enters that scope before calling the function, and exits the scope after the function returns. If you do not want to enter any scope, pass `None`.

## Implementing custom scopes

If the default scopes don’t fit your needs, you can define custom scopes by creating a `ContextScope` object:

```python
from that_depends.providers.context_resources import ContextScope

CUSTOM = ContextScope("CUSTOM")
```

If you want to group all of your scopes in one place, you can extend the `ContextScopes` class:

```python
from that_depends.providers.context_resources import ContextScopes, ContextScope

class MyContextScopes(ContextScopes):
CUSTOM = ContextScope("CUSTOM")
```

## Named scopes with middleware
You can pass a named scope to the `DIContextMiddleware` to set the scope and pre-initialize scoped `ContextResources` for the entire request:

```python
middleware = DIContextMiddleware(app, scope=ContextScopes.REQUEST)
```
18 changes: 17 additions & 1 deletion docs/migration/v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ If you want to learn more about the new features introduced in `2.*`, please ref
await MyContainer.init_resources()
```

2. **`that_depends.providers.AsyncResource` removed**
2. **`that_depends.providers.AsyncResource` removed**

The `AsyncResource` class has been removed. Use `providers.Resource` instead.

**Example:**
Expand Down Expand Up @@ -83,6 +84,21 @@ If you want to learn more about the new features introduced in `2.*`, please ref

> **Note:** `reset_all_containers=True` only reinitializes the context for `ContextResource` instances defined within containers (i.e., classes inheriting from `BaseContainer`). If you also need to reset contexts for resources defined outside containers, you must handle these explicitly. See the [ContextResource documentation](../providers/context-resources.md) for more details.

3. **Container classes now require you to define `default_scope`**

In `2.*`, you must define the `default_scope` attribute in your container classes if you plan to define any `ContextResource` providers in that class. This attribute specifies the default scope for all `ContextResource` providers defined within the container.

**Example:**

```python
from that_depends import BaseContainer, providers

class MyContainer(BaseContainer):
default_scope = None # This will maintain compatibility with 1.*
p = providers.ContextResource(my_resource)
```
Setting the value of `default_context = None` maintains the same behaviours as in `1.*`. Please look at the [scopes Documentation](../introduction/scopes.md).

---

## Potential Issues with `container_context()`
Expand Down
4 changes: 3 additions & 1 deletion docs/providers/context-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ async with container_context():

### Resolving resources whenever a function is called

`container_context` can be used as a decorator:
`ContextResource.context()` can also be used as a decorator:
```python
@MyContainer.session.context # wrap with a session-specific context
@inject
Expand All @@ -207,6 +207,8 @@ Each time you call `await insert_into_database()`, a new instance of `session` w
| Reset all resources in a container | `async with container_context(my_container):` | `async with my_container.async_context():` | `@my_container.context` |
| Reset all sync resources in a container | `with container_context(my_container):` | `with my_container.sync_context():` | `@my_container.context` |

> **Note:** the `context()` wrapper is technically not part of the `SupportsContext` API, however all classes which
> implement this `SupportsContext` also implement this method.
---

## Middleware
Expand Down
1 change: 1 addition & 0 deletions tests/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class SingletonFactory:


class DIContainer(BaseContainer):
default_scope = None
sync_resource = providers.Resource(create_sync_resource)
async_resource = providers.Resource(create_async_resource)

Expand Down
Loading