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
53 changes: 40 additions & 13 deletions src/inject/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,21 +282,48 @@ def __call__(self) -> T:
return self._instance


class _AttributeInjection(Generic[T]):
# NOTE(pyctrl): we MUST inherit `_AttributeInjection` from `property`
# 0. (personal opinion, based on a bunch of cases including this one)
# dataclasses are mess
# 1. dataclasses treat all non-`property` descriptors by the very specific logic
# https://docs.python.org/3/library/dataclasses.html#descriptor-typed-fields
# 2. and treat `property` descriptors in a special way — like we used to know:
# ```
# @dataclass
# class MyDataclass:
# @property
# def my_prop(self) -> int:
# return 42
# MyDataclass.my_prop # gives '<property at 0x73055337f150>' on class
# MyDataclass().my_prop # and on instance will show you '42'
# ```
# it behaves the same in the case of alternative notation:
# ```
# @dataclass
# class MyDataclass2:
# my_prop = property(fget=lambda _: 42)
# MyDataclass2.my_prop # gives '<property at 0x73055337ec00>' on class
# MyDataclass2().my_prop # and on instance will show you '42'
# ```
# which is more relevant to the `inject.attr` case
# 3. but the behavior around `property`-ies has an exception
# - you can't annotate `property` attribute when using the second notation
# (this one `my_prop: int = property(fget=lambda _: 42)` will fail)
# - so the type hinting the very matters
# - in this case dataclasses don't treat class member as property
# (even if it's inherited from `property` or used directly)
# - dataclasses behave greedy when discover their attributes
# and class member annotations are "must have" markers
# 4. so for `inject.attr`s case we should follow 2 rules:
# - `attr` implementation is inherited from `property`
# - `attr` class member is not annotated
class _AttributeInjection(property):
def __init__(self, cls: Type[T] | Hashable) -> None:
self._cls = cls

@overload
def __get__(self, obj: None, owner: Any) -> Self: ...

@overload
def __get__(self, obj: Hashable, owner: Any) -> Injectable: ...

def __get__(self, obj, owner):
if obj is None:
return self

return instance(self._cls)
super().__init__(
fget=lambda _: instance(self._cls),
doc="Return an attribute injection",
)


class _ParameterInjection(Generic[T]):
Expand Down
9 changes: 9 additions & 0 deletions test/test_attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class MyDataClass:

class MyClass:
field = inject.attr(int)
field2: int = inject.attr(int)

inject.configure(lambda binder: binder.bind(int, 123))
my = MyClass()
Expand All @@ -25,6 +26,14 @@ class MyClass:
assert value2 == 123
assert value3 == 123

def test_invalid_attachment_to_dataclass(self):
@dataclass
class MyDataClass:
# dataclasses treat this definition as regular descriptor
field: int = inject.attr(int)

self.assertRaises(AttributeError, MyDataClass)

def test_class_attr(self):
descriptor = inject.attr(int)

Expand Down