From b0fabaf0aeced54a1174f5994bc522a8a02e76de Mon Sep 17 00:00:00 2001 From: Dima Burmistrov Date: Mon, 18 Aug 2025 18:07:25 +0400 Subject: [PATCH] Change attr implementation to be a property --- src/inject/__init__.py | 53 +++++++++++++++++++++++++++++++----------- test/test_attr.py | 9 +++++++ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/inject/__init__.py b/src/inject/__init__.py index 02c862f..88db0d1 100644 --- a/src/inject/__init__.py +++ b/src/inject/__init__.py @@ -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 '' 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 '' 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]): diff --git a/test/test_attr.py b/test/test_attr.py index 5fd8199..93cbbe3 100644 --- a/test/test_attr.py +++ b/test/test_attr.py @@ -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() @@ -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)