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

Patch release 0.12.x #3121

Merged
merged 15 commits into from
Jul 24, 2024
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ repos:
- id: ruff
stages: [commit]
args: [--fix, --exit-non-zero-on-fix]
# Run the formatter
- id: ruff-format
stages: [commit]
16 changes: 8 additions & 8 deletions aries_cloudagent/admin/request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ def __init__(
self,
profile: Profile,
*,
context: InjectionContext = None,
settings: Mapping[str, object] = None,
root_profile: Profile = None,
metadata: dict = None
context: Optional[InjectionContext] = None,
settings: Optional[Mapping[str, object]] = None,
root_profile: Optional[Profile] = None,
metadata: Optional[dict] = None
):
"""Initialize an instance of AdminRequestContext."""
self._context = (context or profile.context).start_scope("admin", settings)
self._context = (context or profile.context).start_scope(settings)
self._profile = profile
self._root_profile = root_profile
self._metadata = metadata
Expand Down Expand Up @@ -72,7 +72,7 @@ def transaction(self) -> ProfileSession:
def inject(
self,
base_cls: Type[InjectType],
settings: Mapping[str, object] = None,
settings: Optional[Mapping[str, object]] = None,
) -> InjectType:
"""Get the provided instance of a given class identifier.

Expand All @@ -89,7 +89,7 @@ def inject(
def inject_or(
self,
base_cls: Type[InjectType],
settings: Mapping[str, object] = None,
settings: Optional[Mapping[str, object]] = None,
default: Optional[InjectType] = None,
) -> Optional[InjectType]:
"""Get the provided instance of a given class identifier or default if not found.
Expand All @@ -111,7 +111,7 @@ def update_settings(self, settings: Mapping[str, object]):

@classmethod
def test_context(
cls, session_inject: dict = None, profile: Profile = None
cls, session_inject: Optional[dict] = None, profile: Optional[Profile] = None
) -> "AdminRequestContext":
"""Quickly set up a new admin request context for tests."""
ctx = AdminRequestContext(profile or IN_MEM.resolved.test_profile())
Expand Down
2 changes: 1 addition & 1 deletion aries_cloudagent/askar/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def bind_providers(self):
"aries_cloudagent.indy.credx.issuer.IndyCredxIssuer", ref(self)
),
)
injector.bind_provider(
injector.soft_bind_provider(
VCHolder,
ClassProvider(
"aries_cloudagent.storage.vc_holder.askar.AskarVCHolder",
Expand Down
47 changes: 7 additions & 40 deletions aries_cloudagent/config/injection_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ class InjectionContext(BaseInjector):
ROOT_SCOPE = "application"

def __init__(
self, *, settings: Mapping[str, object] = None, enforce_typing: bool = True
self,
*,
settings: Optional[Mapping[str, object]] = None,
enforce_typing: bool = True
):
"""Initialize a `ServiceConfig`."""
self._injector = Injector(settings, enforce_typing=enforce_typing)
self._scope_name = InjectionContext.ROOT_SCOPE
self._scopes = []

@property
def injector(self) -> Injector:
Expand All @@ -38,16 +40,6 @@ def injector(self, injector: Injector):
"""Setter for scope-specific injector."""
self._injector = injector

@property
def scope_name(self) -> str:
"""Accessor for the current scope name."""
return self._scope_name

@scope_name.setter
def scope_name(self, scope_name: str):
"""Accessor for the current scope name."""
self._scope_name = scope_name

@property
def settings(self) -> Settings:
"""Accessor for scope-specific settings."""
Expand All @@ -64,7 +56,7 @@ def update_settings(self, settings: Mapping[str, object]):
self.injector.settings.update(settings)

def start_scope(
self, scope_name: str, settings: Optional[Mapping[str, object]] = None
self, settings: Optional[Mapping[str, object]] = None
) -> "InjectionContext":
"""Begin a new named scope.

Expand All @@ -76,39 +68,15 @@ def start_scope(
A new injection context representing the scope

"""
if not scope_name:
raise InjectionContextError("Scope name must be non-empty")
if self._scope_name == scope_name:
raise InjectionContextError("Cannot re-enter scope: {}".format(scope_name))
for scope in self._scopes:
if scope.name == scope_name:
raise InjectionContextError(
"Cannot re-enter scope: {}".format(scope_name)
)
result = self.copy()
result._scopes.append(Scope(name=self.scope_name, injector=self.injector))
result._scope_name = scope_name
if settings:
result.update_settings(settings)
return result

def injector_for_scope(self, scope_name: str) -> Injector:
"""Fetch the injector for a specific scope.

Args:
scope_name: The unique scope identifier
"""
if scope_name == self.scope_name:
return self.injector
for scope in self._scopes:
if scope.name == scope_name:
return scope.injector
return None

def inject(
self,
base_cls: Type[InjectType],
settings: Mapping[str, object] = None,
settings: Optional[Mapping[str, object]] = None,
) -> InjectType:
"""Get the provided instance of a given class identifier.

Expand All @@ -125,7 +93,7 @@ def inject(
def inject_or(
self,
base_cls: Type[InjectType],
settings: Mapping[str, object] = None,
settings: Optional[Mapping[str, object]] = None,
default: Optional[InjectType] = None,
) -> Optional[InjectType]:
"""Get the provided instance of a given class identifier or default if not found.
Expand All @@ -145,5 +113,4 @@ def copy(self) -> "InjectionContext":
"""Produce a copy of the injector instance."""
result = copy.copy(self)
result._injector = self.injector.copy()
result._scopes = self._scopes.copy()
return result
31 changes: 26 additions & 5 deletions aries_cloudagent/config/injector.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Standard Injector implementation."""

from typing import Mapping, Optional, Type
from typing import Dict, Mapping, Optional, Type

from .base import BaseProvider, BaseInjector, InjectionError, InjectType
from .provider import InstanceProvider, CachedProvider
Expand All @@ -11,11 +11,14 @@ class Injector(BaseInjector):
"""Injector implementation with static and dynamic bindings."""

def __init__(
self, settings: Mapping[str, object] = None, *, enforce_typing: bool = True
self,
settings: Optional[Mapping[str, object]] = None,
*,
enforce_typing: bool = True,
):
"""Initialize an `Injector`."""
self.enforce_typing = enforce_typing
self._providers = {}
self._providers: Dict[Type, BaseProvider] = {}
self._settings = Settings(settings)

@property
Expand All @@ -42,6 +45,24 @@ def bind_provider(
provider = CachedProvider(provider)
self._providers[base_cls] = provider

def soft_bind_instance(self, base_cls: Type[InjectType], instance: InjectType):
"""Add a static instance as a soft class binding.

The binding occurs only if a provider for the same type does not already exist.
"""
if not self.get_provider(base_cls):
self.bind_instance(base_cls, instance)

def soft_bind_provider(
self, base_cls: Type[InjectType], provider: BaseProvider, *, cache: bool = False
):
"""Add a dynamic instance resolver as a soft class binding.

The binding occurs only if a provider for the same type does not already exist.
"""
if not self.get_provider(base_cls):
self.bind_provider(base_cls, provider, cache=cache)

def clear_binding(self, base_cls: Type[InjectType]):
"""Remove a previously-added binding."""
if base_cls in self._providers:
Expand All @@ -54,7 +75,7 @@ def get_provider(self, base_cls: Type[InjectType]):
def inject_or(
self,
base_cls: Type[InjectType],
settings: Mapping[str, object] = None,
settings: Optional[Mapping[str, object]] = None,
default: Optional[InjectType] = None,
) -> Optional[InjectType]:
"""Get the provided instance of a given class identifier or default if not found.
Expand Down Expand Up @@ -92,7 +113,7 @@ def inject_or(
def inject(
self,
base_cls: Type[InjectType],
settings: Mapping[str, object] = None,
settings: Optional[Mapping[str, object]] = None,
) -> InjectType:
"""Get the provided instance of a given class identifier.

Expand Down
32 changes: 3 additions & 29 deletions aries_cloudagent/config/tests/test_injection_context.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from unittest import IsolatedAsyncioTestCase

from ..base import InjectionError
from ..injection_context import InjectionContext, InjectionContextError
from ..injection_context import InjectionContext


class TestInjectionContext(IsolatedAsyncioTestCase):
Expand All @@ -14,39 +14,16 @@ def setUp(self):

def test_settings_init(self):
"""Test settings initialization."""
assert self.test_instance.scope_name == self.test_instance.ROOT_SCOPE
for key in self.test_settings:
assert key in self.test_instance.settings
assert self.test_instance.settings[key] == self.test_settings[key]

def test_simple_scope(self):
"""Test scope entrance and exit."""
with self.assertRaises(InjectionContextError):
self.test_instance.start_scope(None)
with self.assertRaises(InjectionContextError):
self.test_instance.start_scope(self.test_instance.ROOT_SCOPE)

injector = self.test_instance.injector_for_scope(self.test_instance.ROOT_SCOPE)
assert injector == self.test_instance.injector
assert self.test_instance.injector_for_scope("no such scope") is None

context = self.test_instance.start_scope(self.test_scope)
assert context.scope_name == self.test_scope
context.scope_name = "Bob"
assert context.scope_name == "Bob"

with self.assertRaises(InjectionContextError):
context.start_scope(self.test_instance.ROOT_SCOPE)
assert self.test_instance.scope_name == self.test_instance.ROOT_SCOPE

def test_settings_scope(self):
"""Test scoped settings."""
upd_settings = {self.test_key: "NEWVAL"}
context = self.test_instance.start_scope(self.test_scope, upd_settings)
context = self.test_instance.start_scope(upd_settings)
assert context.settings[self.test_key] == "NEWVAL"
assert self.test_instance.settings[self.test_key] == self.test_value
root = context.injector_for_scope(context.ROOT_SCOPE)
assert root.settings[self.test_key] == self.test_value

context.settings = upd_settings
assert context.settings == upd_settings
Expand All @@ -64,11 +41,8 @@ async def test_inject_simple(self):

async def test_inject_scope(self):
"""Test a scoped injection."""
context = self.test_instance.start_scope(self.test_scope)
context = self.test_instance.start_scope()
assert context.inject_or(str) is None
context.injector.bind_instance(str, self.test_value)
assert context.inject(str) is self.test_value
assert self.test_instance.inject_or(str) is None
root = context.injector_for_scope(context.ROOT_SCOPE)
assert root.inject_or(str) is None
assert self.test_instance.inject_or(str) is None
33 changes: 33 additions & 0 deletions aries_cloudagent/config/tests/test_injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,39 @@ def test_inject_provider(self):
assert mock_provider.settings[self.test_key] == override_settings[self.test_key]
assert mock_provider.injector is self.test_instance

def test_inject_soft_provider_bindings(self):
"""Test injecting providers with soft binding."""
provider = MockProvider(self.test_value)
override = MockProvider("Override")

self.test_instance.soft_bind_provider(str, provider)
assert self.test_instance.inject(str) == self.test_value

self.test_instance.clear_binding(str)
# Bound by a plugin on startup, for example
self.test_instance.bind_provider(str, override)

# Bound later in Profile.bind_providerse
self.test_instance.soft_bind_provider(str, provider)

# We want the plugin value, not the Profile bound value
assert self.test_instance.inject(str) == "Override"

def test_inject_soft_instance_bindings(self):
"""Test injecting providers with soft binding."""
self.test_instance.soft_bind_instance(str, self.test_value)
assert self.test_instance.inject(str) == self.test_value

self.test_instance.clear_binding(str)
# Bound by a plugin on startup, for example
self.test_instance.bind_instance(str, "Override")

# Bound later in Profile.bind_providerse
self.test_instance.soft_bind_instance(str, self.test_value)

# We want the plugin value, not the Profile bound value
assert self.test_instance.inject(str) == "Override"

def test_bad_provider(self):
"""Test empty and invalid provider results."""
self.test_instance.bind_provider(str, MockProvider(None))
Expand Down
Loading