Skip to content

Commit

Permalink
Merge pull request #3121 from jamshale/patch-release-0.12.x
Browse files Browse the repository at this point in the history
Patch release 0.12.x
  • Loading branch information
swcurran authored Jul 24, 2024
2 parents f9d9bad + 0e61624 commit 4e20667
Show file tree
Hide file tree
Showing 51 changed files with 410 additions and 241 deletions.
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

0 comments on commit 4e20667

Please sign in to comment.