Skip to content

Commit b0fabaf

Browse files
committed
Change attr implementation to be a property
1 parent df5f478 commit b0fabaf

File tree

2 files changed

+49
-13
lines changed

2 files changed

+49
-13
lines changed

src/inject/__init__.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -282,21 +282,48 @@ def __call__(self) -> T:
282282
return self._instance
283283

284284

285-
class _AttributeInjection(Generic[T]):
285+
# NOTE(pyctrl): we MUST inherit `_AttributeInjection` from `property`
286+
# 0. (personal opinion, based on a bunch of cases including this one)
287+
# dataclasses are mess
288+
# 1. dataclasses treat all non-`property` descriptors by the very specific logic
289+
# https://docs.python.org/3/library/dataclasses.html#descriptor-typed-fields
290+
# 2. and treat `property` descriptors in a special way — like we used to know:
291+
# ```
292+
# @dataclass
293+
# class MyDataclass:
294+
# @property
295+
# def my_prop(self) -> int:
296+
# return 42
297+
# MyDataclass.my_prop # gives '<property at 0x73055337f150>' on class
298+
# MyDataclass().my_prop # and on instance will show you '42'
299+
# ```
300+
# it behaves the same in the case of alternative notation:
301+
# ```
302+
# @dataclass
303+
# class MyDataclass2:
304+
# my_prop = property(fget=lambda _: 42)
305+
# MyDataclass2.my_prop # gives '<property at 0x73055337ec00>' on class
306+
# MyDataclass2().my_prop # and on instance will show you '42'
307+
# ```
308+
# which is more relevant to the `inject.attr` case
309+
# 3. but the behavior around `property`-ies has an exception
310+
# - you can't annotate `property` attribute when using the second notation
311+
# (this one `my_prop: int = property(fget=lambda _: 42)` will fail)
312+
# - so the type hinting the very matters
313+
# - in this case dataclasses don't treat class member as property
314+
# (even if it's inherited from `property` or used directly)
315+
# - dataclasses behave greedy when discover their attributes
316+
# and class member annotations are "must have" markers
317+
# 4. so for `inject.attr`s case we should follow 2 rules:
318+
# - `attr` implementation is inherited from `property`
319+
# - `attr` class member is not annotated
320+
class _AttributeInjection(property):
286321
def __init__(self, cls: Type[T] | Hashable) -> None:
287322
self._cls = cls
288-
289-
@overload
290-
def __get__(self, obj: None, owner: Any) -> Self: ...
291-
292-
@overload
293-
def __get__(self, obj: Hashable, owner: Any) -> Injectable: ...
294-
295-
def __get__(self, obj, owner):
296-
if obj is None:
297-
return self
298-
299-
return instance(self._cls)
323+
super().__init__(
324+
fget=lambda _: instance(self._cls),
325+
doc="Return an attribute injection",
326+
)
300327

301328

302329
class _ParameterInjection(Generic[T]):

test/test_attr.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class MyDataClass:
1111

1212
class MyClass:
1313
field = inject.attr(int)
14+
field2: int = inject.attr(int)
1415

1516
inject.configure(lambda binder: binder.bind(int, 123))
1617
my = MyClass()
@@ -25,6 +26,14 @@ class MyClass:
2526
assert value2 == 123
2627
assert value3 == 123
2728

29+
def test_invalid_attachment_to_dataclass(self):
30+
@dataclass
31+
class MyDataClass:
32+
# dataclasses treat this definition as regular descriptor
33+
field: int = inject.attr(int)
34+
35+
self.assertRaises(AttributeError, MyDataClass)
36+
2837
def test_class_attr(self):
2938
descriptor = inject.attr(int)
3039

0 commit comments

Comments
 (0)