Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 8 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ on:
types:
- created

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ]
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]

steps:
- uses: actions/checkout@v6
Expand All @@ -38,9 +37,11 @@ jobs:

lint:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ]
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]

steps:
- uses: actions/checkout@v6
Expand All @@ -66,6 +67,8 @@ jobs:
needs: [ test, lint ]
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' }}
permissions:
contents: read

steps:
- uses: actions/checkout@v6
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Opyoid follows [semver guidelines](https://semver.org) for versioning.

## Unreleased
## 3.0.4
### Fixes
- Fix dependency loops not always raising a `CyclicDependencyError` and crashing with a `maximum recursion depth exceeded`

## 3.0.3
### Fixes
- Fixed a crash when using Instance Bindings with objects without an equality operator, such as pandas DataFrames or
Expand Down
9 changes: 6 additions & 3 deletions opyoid/bindings/self_binding/callable_to_provider_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,12 @@ def _get_provider(
targets: List[Target[InjectedT]], parent_context: InjectionContext[Any]
) -> Optional[Provider[InjectedT]]:
for target_index, target in enumerate(targets):
context = parent_context.get_child_context(target, allow_jit_provider=target_index == len(targets) - 1)
context.current_class = parent_context.current_class
context.current_parameter = parent_context.current_parameter
context = parent_context.get_child_context(
target,
allow_jit_provider=target_index == len(targets) - 1,
current_class=parent_context.current_class,
current_parameter=parent_context.current_parameter,
)
try:
return context.get_provider()
except NoBindingFound:
Expand Down
13 changes: 10 additions & 3 deletions opyoid/injection_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .utils import InjectedT

if TYPE_CHECKING:
from .bindings import Binding, RegisteredBinding
from .bindings import RegisteredBinding
from .injection_state import InjectionState


Expand Down Expand Up @@ -47,9 +47,16 @@ def _dependency_chain(self) -> List[Target[Any]]:
return chain

def get_child_context(
self, new_target: Target[InjectedSubT], allow_jit_provider: bool = True
self,
new_target: Target[InjectedSubT],
*,
allow_jit_provider: bool = True,
current_class: Optional[Type[InjectedSubT]] = None,
current_parameter: Optional[Parameter] = None,
) -> "InjectionContext[InjectedSubT]":
return InjectionContext(new_target, self.injection_state, self, allow_jit_provider)
return InjectionContext(
new_target, self.injection_state, self, allow_jit_provider, current_class, current_parameter
)

def get_new_state_context(self, new_state: "InjectionState") -> "InjectionContext[InjectedT]":
return InjectionContext(self.target, new_state, self.parent_context, self.allow_jit_provider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ def create(self, context: InjectionContext[InjectedT]) -> Provider[InjectedT]:
for subtype in cast(Union[InjectedT], context.target.type).__args__:
try:
new_target: Target[InjectedT] = Target(subtype, context.target.named)
new_context = context.get_child_context(new_target)
new_context.current_class = context.current_class
new_context.current_parameter = context.current_parameter
new_context = context.get_child_context(
new_target, current_class=context.current_class, current_parameter=context.current_parameter
)
return new_context.get_provider()
except NoBindingFound:
pass
Expand Down
23 changes: 23 additions & 0 deletions tests_e2e/test_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,29 @@ def __init__(self, arg: MyOtherClass):
with self.assertRaises(CyclicDependencyError):
Injector(bindings=[ClassBinding(MyClass, MyImpl), SelfBinding(MyOtherClass)])

def test_list_circular_injection_raises_error(self):
class ParentClass:
def __init__(self, children: List[MyClass]):
self.children = children

class SubClass2(MyClass):
def __init__(self, parent: ParentClass):
self.parent = parent

class SubClass1(MyClass):
pass

class MyModule(Module):
def configure(self):
self.bind(ParentClass)
self.multi_bind(
MyClass,
[self.bind_item(to_class=SubClass1), self.bind_item(to_class=SubClass2)],
)

with self.assertRaises(CyclicDependencyError):
Injector([MyModule])

def test_cyclic_dependencies_with_private_module_are_handled(self):
class MyOtherClass:
def __init__(self, arg: MyClass):
Expand Down