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 11 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
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
151 changes: 151 additions & 0 deletions tests/providers/test_context_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

from that_depends import BaseContainer, Provide, fetch_context_item, inject, providers
from that_depends.entities.resource_context import ResourceContext
from that_depends.meta import DefaultScopeNotDefinedError
from that_depends.providers import container_context
from that_depends.providers.context_resources import ContextScope, _enter_named_scope, get_current_scope


logger = logging.getLogger(__name__)
Expand All @@ -30,6 +32,7 @@ async def create_async_context_resource() -> typing.AsyncIterator[str]:


class DIContainer(BaseContainer):
default_scope = None
sync_context_resource = providers.ContextResource(create_sync_context_resource)
async_context_resource = providers.ContextResource(create_async_context_resource)
dynamic_context_resource = providers.Selector(
Expand All @@ -40,6 +43,7 @@ class DIContainer(BaseContainer):


class DependentDiContainer(BaseContainer):
default_scope = None
dependent_sync_context_resource = providers.ContextResource(create_sync_context_resource)
dependent_async_context_resource = providers.ContextResource(create_async_context_resource)

Expand Down Expand Up @@ -618,6 +622,7 @@ async def slow_async_creator() -> typing.AsyncIterator[str]:
yield str(uuid.uuid4())

class MyContainer(BaseContainer):
default_scope = None
slow_provider = providers.ContextResource(slow_async_creator)

async def _injected() -> str:
Expand All @@ -633,6 +638,7 @@ def slow_sync_creator() -> typing.Iterator[str]:
yield str(uuid.uuid4())

class MyContainer(BaseContainer):
default_scope = None
slow_provider = providers.ContextResource(slow_sync_creator)

def _injected() -> str:
Expand All @@ -646,3 +652,148 @@ def _injected() -> str:

for thread in threads:
thread.join()


def test_default_named_scope_is_none() -> None:
assert get_current_scope() is None


def test_entering_scope_sets_current_scope() -> None:
with _enter_named_scope(ContextScope.FUNCTION):
assert get_current_scope() == ContextScope.FUNCTION
assert get_current_scope() is None


def test_entering_scope_with_container_context_sync() -> None:
with container_context(scope=ContextScope.FUNCTION):
assert get_current_scope() == ContextScope.FUNCTION
assert get_current_scope() is None


async def test_entering_scope_with_container_context_async() -> None:
async with container_context(scope=ContextScope.FUNCTION):
assert get_current_scope() == ContextScope.FUNCTION
assert get_current_scope() is None


def test_scoped_provider_get_scope() -> None:
provider = providers.ContextResource(create_async_context_resource)
assert provider.get_scope() == ContextScope.ANY
provider = provider.with_config(scope=ContextScope.FUNCTION)
assert provider.get_scope() == ContextScope.FUNCTION


def test_scoped_container_get_scope() -> None:
class _Container(BaseContainer): ...

assert _Container.get_scope() is ContextScope.ANY

class _ScopedContainer(BaseContainer):
default_scope = ContextScope.FUNCTION

assert _ScopedContainer.get_scope() == ContextScope.FUNCTION


def test_sync_resolve_scoped_resource() -> None:
provider = providers.ContextResource(create_sync_context_resource).with_config(scope=ContextScope.FUNCTION)
with pytest.raises(RuntimeError), provider.sync_context():
assert provider.sync_resolve() is not None
with pytest.raises(RuntimeError):
provider.sync_resolve()

with container_context(provider, scope=ContextScope.FUNCTION):
assert provider.sync_resolve() is not None


async def test_async_resolve_scoped_resource() -> None:
provider = providers.ContextResource(create_async_context_resource).with_config(scope=ContextScope.FUNCTION)
with pytest.raises(RuntimeError):
async with provider.async_context():
assert await provider.async_resolve() is not None
with pytest.raises(RuntimeError):
await provider.async_resolve()

async with container_context(provider, scope=ContextScope.FUNCTION):
assert await provider.async_resolve() is not None


async def test_async_resolve_non_scoped_in_named_context() -> None:
provider = providers.ContextResource(create_async_context_resource)
async with container_context(provider, scope=ContextScope.FUNCTION):
assert await provider.async_resolve() is not None


def test_sync_resolve_non_scoped_in_named_context() -> None:
provider = providers.ContextResource(create_sync_context_resource)
with container_context(provider, scope=ContextScope.FUNCTION):
assert provider.sync_resolve() is not None


async def test_async_container_init_context_for_scoped_resources() -> None:
class _Container(BaseContainer):
async_resource = providers.ContextResource(create_async_context_resource).with_config(
scope=ContextScope.FUNCTION
)

async with container_context(scope=ContextScope.FUNCTION):
assert await _Container.async_resource.async_resolve() is not None
async with container_context(scope=None):
with pytest.raises(RuntimeError):
await _Container.async_resource.async_resolve()


def test_sync_container_init_context_for_scoped_resources() -> None:
class _Container(BaseContainer):
sync_resource = providers.ContextResource(create_sync_context_resource).with_config(scope=ContextScope.FUNCTION)

with container_context(scope=ContextScope.FUNCTION):
assert _Container.sync_resource.sync_resolve() is not None
with container_context(scope=None), pytest.raises(RuntimeError):
_Container.sync_resource.sync_resolve()


async def test_sync_container_init_context_for_default_container_resources() -> None:
class _Container(BaseContainer):
default_scope = ContextScope.FUNCTION
sync_resource = providers.ContextResource(create_sync_context_resource)

assert _Container.sync_resource.get_scope() == ContextScope.FUNCTION
with container_context(scope=ContextScope.FUNCTION):
assert _Container.sync_resource.sync_resolve() is not None
with pytest.raises(RuntimeError), container_context(scope=None):
assert _Container.sync_resource.sync_resolve() is not None


def test_container_with_context_resources_must_have_default_scope_set() -> None:
with pytest.raises(DefaultScopeNotDefinedError):

class _Container(BaseContainer):
sync_resource = providers.ContextResource(create_sync_context_resource)


def test_providers_with_explicit_scope_ignore_default_scope() -> None:
class _Container(BaseContainer):
default_scope = None
sync_resource = providers.ContextResource(create_sync_context_resource).with_config(scope=ContextScope.FUNCTION)

assert _Container.sync_resource.get_scope() == ContextScope.FUNCTION


async def test_none_scoped_provider_should_not_be_resolvable_in_named_scope_async() -> None:
provider = providers.ContextResource(create_async_context_resource).with_config(scope=None)
async with container_context(scope=ContextScope.FUNCTION):
with pytest.raises(RuntimeError):
await provider.async_resolve()


def test_none_scoped_provider_should_not_be_resolvable_in_named_scope_sync() -> None:
provider = providers.ContextResource(create_sync_context_resource).with_config(scope=None)
with container_context(scope=ContextScope.FUNCTION), pytest.raises(RuntimeError):
provider.sync_resolve()


def test_container_context_does_not_support_scope_any() -> None:
with (
pytest.raises(ValueError, match=f"{ContextScope.ANY} cannot be entered!"),
):
container_context(scope=ContextScope.ANY)
50 changes: 47 additions & 3 deletions tests/test_injection.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import asyncio
import datetime
import typing
import warnings

import pytest

from tests import container
from that_depends import Provide, inject
from that_depends import BaseContainer, Provide, inject, providers
from that_depends.providers.context_resources import ContextScope


@pytest.fixture(name="fixture_one")
def create_fixture_one() -> int:
return 1


async def _async_creator() -> typing.AsyncIterator[int]:
yield 1


def _sync_creator() -> typing.Iterator[int]:
yield 1


@inject
async def test_injection(
fixture_one: int,
Expand Down Expand Up @@ -73,8 +83,7 @@ def inner(
return _

factory = container.SimpleFactory(dep1="1", dep2=2)
with pytest.warns(RuntimeWarning, match="Expected injection, but nothing found. Remove @inject decorator."):
assert inner(_=factory) == factory
assert inner(_=factory) == factory


def test_sync_empty_injection() -> None:
Expand All @@ -94,3 +103,38 @@ async def main(simple_factory: container.SimpleFactory = Provide[container.DICon
assert simple_factory

asyncio.run(main())


async def test_async_injection_with_scope() -> None:
class _Container(BaseContainer):
default_scope = ContextScope.ANY
async_resource = providers.ContextResource(_async_creator).with_config(scope=ContextScope.FUNCTION)

async def _injected(val: int = Provide[_Container.async_resource]) -> int:
return val

assert await inject(scope=ContextScope.FUNCTION)(_injected)() == 1
with pytest.raises(RuntimeError):
await inject(scope=None)(_injected)()
with pytest.raises(RuntimeError):
await inject(_injected)()


async def test_sync_injection_with_scope() -> None:
class _Container(BaseContainer):
default_scope = ContextScope.ANY
sync_resource = providers.ContextResource(_sync_creator).with_config(scope=ContextScope.FUNCTION)

def _injected(val: int = Provide[_Container.sync_resource]) -> int:
return val

assert inject(scope=ContextScope.FUNCTION)(_injected)() == 1
with pytest.raises(RuntimeError):
inject(scope=None)(_injected)()
with pytest.raises(RuntimeError):
inject(_injected)()


def test_inject_decorator_should_not_allow_any_scope() -> None:
with pytest.raises(ValueError, match=f"{ContextScope.ANY} is not allowed in inject decorator."):
inject(scope=ContextScope.ANY)
3 changes: 2 additions & 1 deletion that_depends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
from that_depends.container import BaseContainer
from that_depends.injection import Provide, inject
from that_depends.providers import container_context
from that_depends.providers.context_resources import fetch_context_item
from that_depends.providers.context_resources import fetch_context_item, get_current_scope


__all__ = [
"BaseContainer",
"Provide",
"container_context",
"fetch_context_item",
"get_current_scope",
"inject",
"providers",
]
9 changes: 8 additions & 1 deletion that_depends/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from that_depends.meta import BaseContainerMeta
from that_depends.providers import AbstractProvider, Resource, Singleton
from that_depends.providers.context_resources import ContextResource, SupportsContext
from that_depends.providers.context_resources import ContextResource, ContextScope, SupportsContext


if typing.TYPE_CHECKING:
Expand All @@ -22,6 +22,13 @@ class BaseContainer(SupportsContext[None], metaclass=BaseContainerMeta):

providers: dict[str, AbstractProvider[typing.Any]]
containers: list[type["BaseContainer"]]
default_scope: ContextScope | None = ContextScope.ANY

@classmethod
@override
def get_scope(cls) -> ContextScope | None:
"""Get default container scope."""
return cls.default_scope

@override
def __new__(cls, *_: typing.Any, **__: typing.Any) -> "typing_extensions.Self":
Expand Down
Loading