Skip to content

Commit d82d9fb

Browse files
authored
Improve debugability of deepcopy errors (#839)
1 parent 3ba4704 commit d82d9fb

File tree

4 files changed

+155
-12
lines changed

4 files changed

+155
-12
lines changed

src/dependency_injector/errors.py

+21
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,24 @@ class Error(Exception):
1010

1111
class NoSuchProviderError(Error, AttributeError):
1212
"""Error that is raised when provider lookup is failed."""
13+
14+
15+
class NonCopyableArgumentError(Error):
16+
"""Error that is raised when provider argument is not deep-copyable."""
17+
18+
index: int
19+
keyword: str
20+
provider: object
21+
22+
def __init__(self, provider: object, index: int = -1, keyword: str = "") -> None:
23+
self.provider = provider
24+
self.index = index
25+
self.keyword = keyword
26+
27+
def __str__(self) -> str:
28+
s = (
29+
f"keyword argument {self.keyword}"
30+
if self.keyword
31+
else f"argument at index {self.index}"
32+
)
33+
return f"Couldn't copy {s} for provider {self.provider!r}"

src/dependency_injector/providers.pyi

+15-1
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,21 @@ def is_delegated(instance: Any) -> bool: ...
530530
def represent_provider(provider: Provider, provides: Any) -> str: ...
531531

532532

533-
def deepcopy(instance: Any, memo: Optional[_Dict[Any, Any]] = None): Any: ...
533+
def deepcopy(instance: Any, memo: Optional[_Dict[Any, Any]] = None) -> Any: ...
534+
535+
536+
def deepcopy_args(
537+
provider: Provider[Any],
538+
args: Tuple[Any, ...],
539+
memo: Optional[_Dict[int, Any]] = None,
540+
) -> Tuple[Any, ...]: ...
541+
542+
543+
def deepcopy_kwargs(
544+
provider: Provider[Any],
545+
kwargs: _Dict[str, Any],
546+
memo: Optional[_Dict[int, Any]] = None,
547+
) -> Dict[str, Any]: ...
534548

535549

536550
def merge_dicts(dict1: _Dict[Any, Any], dict2: _Dict[Any, Any]) -> _Dict[Any, Any]: ...

src/dependency_injector/providers.pyx

+54-11
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ except ImportError:
7171
from .errors import (
7272
Error,
7373
NoSuchProviderError,
74+
NonCopyableArgumentError,
7475
)
7576

7677
cimport cython
@@ -1252,8 +1253,8 @@ cdef class Callable(Provider):
12521253

12531254
copied = _memorized_duplicate(self, memo)
12541255
copied.set_provides(_copy_if_provider(self.provides, memo))
1255-
copied.set_args(*deepcopy(self.args, memo))
1256-
copied.set_kwargs(**deepcopy(self.kwargs, memo))
1256+
copied.set_args(*deepcopy_args(self, self.args, memo))
1257+
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
12571258
self._copy_overridings(copied, memo)
12581259
return copied
12591260

@@ -2539,8 +2540,8 @@ cdef class Factory(Provider):
25392540

25402541
copied = _memorized_duplicate(self, memo)
25412542
copied.set_provides(_copy_if_provider(self.provides, memo))
2542-
copied.set_args(*deepcopy(self.args, memo))
2543-
copied.set_kwargs(**deepcopy(self.kwargs, memo))
2543+
copied.set_args(*deepcopy_args(self, self.args, memo))
2544+
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
25442545
copied.set_attributes(**deepcopy(self.attributes, memo))
25452546
self._copy_overridings(copied, memo)
25462547
return copied
@@ -2838,8 +2839,8 @@ cdef class BaseSingleton(Provider):
28382839

28392840
copied = _memorized_duplicate(self, memo)
28402841
copied.set_provides(_copy_if_provider(self.provides, memo))
2841-
copied.set_args(*deepcopy(self.args, memo))
2842-
copied.set_kwargs(**deepcopy(self.kwargs, memo))
2842+
copied.set_args(*deepcopy_args(self, self.args, memo))
2843+
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
28432844
copied.set_attributes(**deepcopy(self.attributes, memo))
28442845
self._copy_overridings(copied, memo)
28452846
return copied
@@ -3451,7 +3452,7 @@ cdef class List(Provider):
34513452
return copied
34523453

34533454
copied = _memorized_duplicate(self, memo)
3454-
copied.set_args(*deepcopy(self.args, memo))
3455+
copied.set_args(*deepcopy_args(self, self.args, memo))
34553456
self._copy_overridings(copied, memo)
34563457
return copied
34573458

@@ -3674,8 +3675,8 @@ cdef class Resource(Provider):
36743675

36753676
copied = _memorized_duplicate(self, memo)
36763677
copied.set_provides(_copy_if_provider(self.provides, memo))
3677-
copied.set_args(*deepcopy(self.args, memo))
3678-
copied.set_kwargs(**deepcopy(self.kwargs, memo))
3678+
copied.set_args(*deepcopy_args(self, self.args, memo))
3679+
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
36793680

36803681
self._copy_overridings(copied, memo)
36813682

@@ -4525,8 +4526,8 @@ cdef class MethodCaller(Provider):
45254526

45264527
copied = _memorized_duplicate(self, memo)
45274528
copied.set_provides(_copy_if_provider(self.provides, memo))
4528-
copied.set_args(*deepcopy(self.args, memo))
4529-
copied.set_kwargs(**deepcopy(self.kwargs, memo))
4529+
copied.set_args(*deepcopy_args(self, self.args, memo))
4530+
copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo))
45304531
self._copy_overridings(copied, memo)
45314532
return copied
45324533

@@ -4927,6 +4928,48 @@ cpdef object deepcopy(object instance, dict memo=None):
49274928
return copy.deepcopy(instance, memo)
49284929

49294930

4931+
cpdef tuple deepcopy_args(
4932+
Provider provider,
4933+
tuple args,
4934+
dict[int, object] memo = None,
4935+
):
4936+
"""A wrapper for deepcopy for positional arguments.
4937+
4938+
Used to improve debugability of objects that cannot be deep-copied.
4939+
"""
4940+
4941+
cdef list[object] out = []
4942+
4943+
for i, arg in enumerate(args):
4944+
try:
4945+
out.append(copy.deepcopy(arg, memo))
4946+
except Exception as e:
4947+
raise NonCopyableArgumentError(provider, index=i) from e
4948+
4949+
return tuple(out)
4950+
4951+
4952+
cpdef dict[str, object] deepcopy_kwargs(
4953+
Provider provider,
4954+
dict[str, object] kwargs,
4955+
dict[int, object] memo = None,
4956+
):
4957+
"""A wrapper for deepcopy for keyword arguments.
4958+
4959+
Used to improve debugability of objects that cannot be deep-copied.
4960+
"""
4961+
4962+
cdef dict[str, object] out = {}
4963+
4964+
for name, arg in kwargs.items():
4965+
try:
4966+
out[name] = copy.deepcopy(arg, memo)
4967+
except Exception as e:
4968+
raise NonCopyableArgumentError(provider, keyword=name) from e
4969+
4970+
return out
4971+
4972+
49304973
def __add_sys_streams(memo):
49314974
"""Add system streams to memo dictionary.
49324975
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import sys
2+
from typing import Any, Dict, NoReturn
3+
4+
from pytest import raises
5+
6+
from dependency_injector.errors import NonCopyableArgumentError
7+
from dependency_injector.providers import (
8+
Provider,
9+
deepcopy,
10+
deepcopy_args,
11+
deepcopy_kwargs,
12+
)
13+
14+
15+
class NonCopiable:
16+
def __deepcopy__(self, memo: Dict[int, Any]) -> NoReturn:
17+
raise NotImplementedError
18+
19+
20+
def test_deepcopy_streams_not_copied() -> None:
21+
l = [sys.stdin, sys.stdout, sys.stderr]
22+
assert deepcopy(l) == l
23+
24+
25+
def test_deepcopy_args() -> None:
26+
provider = Provider[None]()
27+
copiable = NonCopiable()
28+
memo: Dict[int, Any] = {id(copiable): copiable}
29+
30+
assert deepcopy_args(provider, (1, copiable), memo) == (1, copiable)
31+
32+
33+
def test_deepcopy_args_non_copiable() -> None:
34+
provider = Provider[None]()
35+
copiable = NonCopiable()
36+
memo: Dict[int, Any] = {id(copiable): copiable}
37+
38+
with raises(
39+
NonCopyableArgumentError,
40+
match=r"^Couldn't copy argument at index 3 for provider ",
41+
):
42+
deepcopy_args(provider, (1, copiable, object(), NonCopiable()), memo)
43+
44+
45+
def test_deepcopy_kwargs() -> None:
46+
provider = Provider[None]()
47+
copiable = NonCopiable()
48+
memo: Dict[int, Any] = {id(copiable): copiable}
49+
50+
assert deepcopy_kwargs(provider, {"x": 1, "y": copiable}, memo) == {
51+
"x": 1,
52+
"y": copiable,
53+
}
54+
55+
56+
def test_deepcopy_kwargs_non_copiable() -> None:
57+
provider = Provider[None]()
58+
copiable = NonCopiable()
59+
memo: Dict[int, Any] = {id(copiable): copiable}
60+
61+
with raises(
62+
NonCopyableArgumentError,
63+
match=r"^Couldn't copy keyword argument z for provider ",
64+
):
65+
deepcopy_kwargs(provider, {"x": 1, "y": copiable, "z": NonCopiable()}, memo)

0 commit comments

Comments
 (0)