Skip to content

Commit 4d94386

Browse files
Fix/pep 563 class init annotations parsing (#107)
* fix: handle class init * refactor: improve readability * test: cover annotations parsing in class init * version: bump to new 0.76.0 version * docs: refer to optional annotated cpython issue #90353 --------- Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>
1 parent 133d696 commit 4d94386

File tree

3 files changed

+35
-10
lines changed

3 files changed

+35
-10
lines changed

di/_utils/inspect.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,27 @@ def is_gen_callable(call: Any) -> bool:
4747

4848
def get_annotations(call: Callable[..., Any]) -> Dict[str, Any]:
4949
types_from: Callable[..., Any]
50-
if not (
51-
inspect.isclass(call) or inspect.isfunction(call) or inspect.ismethod(call)
52-
) and hasattr(call, "__call__"):
53-
# callable class
54-
types_from = call.__call__ # type: ignore[misc,operator] # accessing __init__ directly
50+
if inspect.isclass(call):
51+
types_from = call.__init__
52+
elif is_callable_class(call):
53+
types_from = call.__call__ # type: ignore[operator] # mypy issue #5079
5554
else:
56-
# method
5755
types_from = call
56+
5857
hints = get_type_hints(types_from, include_extras=True)
59-
# for no apparent reason, Annotated[Optional[T]] comes back as Optional[Annotated[Optional[T]]]
60-
# so remove the outer Optional if this is the case
58+
hints = fix_annotated_optional_type_hints(hints)
59+
60+
return hints
61+
62+
63+
def is_callable_class(call: Callable[..., Any]) -> bool:
64+
function_or_method = inspect.isfunction(call) or inspect.ismethod(call)
65+
has_call = hasattr(call, "__call__")
66+
return not function_or_method and has_call
67+
68+
69+
def fix_annotated_optional_type_hints(hints: Dict[str, Any]) -> Dict[str, Any]:
70+
"""https://github.com/python/cpython/issues/90353"""
6171
for param_name, hint in hints.items():
6272
args = get_args(hint)
6373
if get_origin(hint) is Union and get_origin(next(iter(args))) is Annotated:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "di"
3-
version = "0.75.4"
3+
version = "0.76.0"
44
description = "Dependency injection toolkit"
55
authors = ["Adrian Garcia Badaracco <adrian@adriangb.com>"]
66
readme = "README.md"

tests/test_wiring_pep_563.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,26 @@ def __call__(self: Test) -> Test:
1010
return self
1111

1212

13-
def test_postponed_evaluation_solving():
13+
def test_postponed_evaluation_solving_in_call():
1414
container = Container()
1515
with container.enter_scope(None) as state:
1616
res = container.solve(Dependent(Test.__call__), scopes=[None]).execute_sync(
1717
executor=SyncExecutor(),
1818
state=state,
1919
)
2020
assert isinstance(res, Test)
21+
22+
23+
class NeedsTest:
24+
def __init__(self, test: Test) -> None:
25+
self.test = test
26+
27+
28+
def test_postponed_evaluation_solving_in_init():
29+
container = Container()
30+
with container.enter_scope(None) as state:
31+
res = container.solve(Dependent(NeedsTest), scopes=[None]).execute_sync(
32+
executor=SyncExecutor(),
33+
state=state,
34+
)
35+
assert isinstance(res, NeedsTest)

0 commit comments

Comments
 (0)