New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dynamically accessing attributes of provided objects. #281
Comments
Hi @JarnoRFB , There was a similar request about method calling some time ago #190 (comment) As you see the last time it ended up with a client code redesigning. You were very close with creating your own provider. You failed only because of the undocumented behaviour (this issue with copying). I'm sending to you a patched version of your provider: class AttributeFactory(providers.Provider):
__slots__ = ('_factory',)
def __init__(self, *args, **kwargs):
self._factory = providers.Factory(*args, **kwargs)
super().__init__()
def __deepcopy__(self, memo):
copy = providers.deepcopy(self._factory, memo)
return self.__class__(
copy.provides,
*copy.args,
**copy.kwargs,
)
def __getattr__(self, item):
return providers.Callable(getattr, self._factory, item)
def _provide(self, args, kwargs):
return self._factory(*args, **kwargs) There are two points:
|
Since this use case is raised second time I would like to get it covered. I've created a little sketch for three different options: import random
from dependency_injector import containers, providers
class B:
def __init__(self):
self.val = random.randint(0, 10)
def val_method(self):
return random.randint(0, 10)
def __repr__(self):
return f"B({self.val})"
class A:
def __init__(self, b_val: int, b2_val):
self.b_val = b_val
self.b2_val = b2_val
def __repr__(self):
return f"A({self.b_val}, {self.b2_val})"
class AttributeGetter(providers.Provider):
def __init__(self, provider, attribute):
self._provider = provider
self._attribute = attribute
super().__init__()
@property
def call(self):
return MethodCaller(self._provider, self._attribute)
def __deepcopy__(self, memo=None):
return self.__class__(providers.deepcopy(self._provider), self._attribute)
def _provide(self, args, kwargs):
provided = self._provider(*args, **kwargs)
return getattr(provided, self._attribute)
class MethodCaller(providers.Provider):
def __init__(self, provider, method):
self._provider = provider
self.method = method
super().__init__()
def __deepcopy__(self, memo=None):
return self.__class__(providers.deepcopy(self._provider), self.method)
def _provide(self, args, kwargs):
provided = self._provider(*args, **kwargs)
method = getattr(provided, self.method)
return method()
class MyFactory(providers.Factory):
def __getattr__(self, item):
return AttributeGetter(self, item)
def attribute(self, item):
return AttributeGetter(self, item)
def method(self, item):
return MethodCaller(self, item)
get_attribute = attribute
call_method = method
class MyContainer(containers.DeclarativeContainer):
b = MyFactory(B)
a = providers.Singleton(
A,
b.attribute('val'),
b.method('val_method'),
)
a2 = providers.Singleton(
A,
AttributeGetter(b, 'val'),
MethodCaller(b, 'val_method'),
)
a3 = providers.Singleton(
A,
b.val,
b.val_method.call,
)
if __name__ == '__main__':
container = MyContainer()
a = container.a()
print(a)
a2 = container.a2()
print(a2)
a3 = container.a2()
print(a2) |
I don't yet know which one of this three options is a good one: class MyContainer(containers.DeclarativeContainer):
b = MyFactory(B)
a = providers.Singleton(
A,
b.attribute('val'),
b.method('val_method'),
)
a2 = providers.Singleton(
A,
AttributeGetter(b, 'val'),
MethodCaller(b, 'val_method'),
)
a3 = providers.Singleton(
A,
b.val,
b.val_method.call,
) Dependency Injector has never used any string identifier so # 1 and # 2 is out of common design. Option # 3 might be a solution. The only thing I worry about with # 3 is that there might be name conflicts between provided instance attributes and provider attributes (especially with provider attributes added later). @JarnoRFB what do you think? PS: if anybody watching this issue and has a thought, you're welcome to join the conversation |
@rmk135 Thanks for the quick response and the nice illustration! I agree with you that using strings is unhandy and breaks the look and feel. I also agree that naming conflicts will be problematic both because of evolving As a fourth option, I could imaging adding an extra namespace to all providers that gives access to attributes, methods and items. For example
import random
from dependency_injector import containers, providers
class B:
def __init__(self):
self.val = random.randint(0, 10)
self.vals = [random.randint(0, 10)]
def val_method(self):
return random.randint(0, 10)
def __repr__(self):
return f"B({self.val, self.vals})"
def __getitem__(self, item):
return self.vals[item]
class A:
def __init__(self, b_val: int, b2_val, b3_val, b4_val):
self.b_val = b_val
self.b2_val = b2_val
self.b3_val = b3_val
self.b4_val = b4_val
def __repr__(self):
return f"A({self.b_val}, {self.b2_val}, {self.b3_val}, {self.b4_val})"
class AttributeGetter(providers.Provider):
def __init__(self, provider, attribute):
self._provider = provider
self._attribute = attribute
super().__init__()
@property
def call(self):
return MethodCaller(self._provider, self._attribute)
def __deepcopy__(self, memo=None):
return self.__class__(providers.deepcopy(self._provider), self._attribute)
def _provide(self, args, kwargs):
provided = self._provider(*args, **kwargs)
return getattr(provided, self._attribute)
def __getitem__(self, item):
return MethodCaller(self, "__getitem__", item)
def __repr__(self):
return f"AttributeGetter({self._attribute})"
class MethodCaller(providers.Provider):
def __init__(self, provider, method, *args, **kwargs):
self._provider = provider
self.method = method
self._args = args
self._kwargs = kwargs
super().__init__()
def __deepcopy__(self, memo=None):
return self.__class__(providers.deepcopy(self._provider), self.method, *self._args, **self._kwargs)
def _provide(self, args, kwargs):
provided = self._provider(*args, **kwargs)
method = getattr(provided, self.method)
return method(*self._args, **self._kwargs)
class MySingleton(providers.Singleton):
@property
def provided(self):
return ProvidedAttributes(self)
class ProvidedAttributes(providers.Provider):
def __init__(self, provider):
self._provider = provider
super().__init__()
def __getattr__(self, item):
return AttributeGetter(self._provider, item)
def __getitem__(self, item):
return MethodCaller(self._provider, "__getitem__", item)
class MyContainer(containers.DeclarativeContainer):
b = MySingleton(B)
a4 = providers.Singleton(
A,
b.provided.val,
b.provided.vals[0],
b.provided.val_method.call(),
b.provided[0],
)
if __name__ == '__main__':
container = MyContainer()
b = container.b()
a4 = container.a4()
print(b)
print(a4)
Note that I exchanged |
Hey @JarnoRFB , That a great design! I'll add this to the
Yep, I also noticed that yesterday. I think about adding PS: Some time ago I've finished reading one cool book. It's TRIZ by Genrich Altshuller. TRIZ (or TIPS) is the theory of inventive problem solving. It stands on finding and fighting different contradictions. The typical (or inherent) contradiction sounds like "Software should be complex (to have many features), but simple (to be easy to learn)". The solution you have proposed is free of negative part of the contradictions and includes the positive parts. TRIZ states the "ideal object" as - the object that does not exist, but its function is handled. Your design looks like an "ideal object" (following TRIZ). Very cool! Thank you |
I reproduced the problem with the improper work of the import random
from dependency_injector import containers, providers
class B:
def __init__(self):
self.val = random.randint(0, 10)
self.vals = [self.val]
def val_method(self):
return self.val
def __repr__(self):
return f"B({self.val, self.vals})"
def __getitem__(self, item):
return self.vals[item]
class A:
def __init__(self, b_val: int, b2_val, b3_val, b4_val, b):
self.b_val = b_val
self.b2_val = b2_val
self.b3_val = b3_val
self.b4_val = b4_val
self.b = b
def __repr__(self):
return f"A({self.b_val}, {self.b2_val}, {self.b3_val}, {self.b4_val}, {self.b})"
class AttributeGetter(providers.Provider):
def __init__(self, provider, attribute):
self._provider = provider
self._attribute = attribute
super().__init__()
def call(self):
return MethodCaller(self._provider, self._attribute)
def __deepcopy__(self, memo=None):
return self.__class__(providers.deepcopy(self._provider, memo), self._attribute)
def __getitem__(self, item):
return MethodCaller(self, "__getitem__", item)
def __repr__(self):
return f"AttributeGetter({self._attribute})"
def _provide(self, args, kwargs):
provided = self._provider(*args, **kwargs)
return getattr(provided, self._attribute)
class MethodCaller(providers.Provider):
def __init__(self, provider, method, *args, **kwargs):
self._provider = provider
self.method = method
self._args = args
self._kwargs = kwargs
super().__init__()
def __deepcopy__(self, memo=None):
return self.__class__(providers.deepcopy(self._provider, memo), self.method, *self._args, **self._kwargs)
def _provide(self, args, kwargs):
provided = self._provider(*args, **kwargs)
method = getattr(provided, self.method)
return method(*self._args, **self._kwargs)
class MySingleton(providers.Singleton):
@property
def provided(self):
return ProvidedAttributes(self)
class ProvidedAttributes(providers.Provider):
def __init__(self, provider):
self._provider = provider
super().__init__()
def __deepcopy__(self, memo=None):
return self.__class__(providers.deepcopy(self._provider, memo))
def __getattr__(self, item):
return AttributeGetter(self._provider, item)
def __getitem__(self, item):
return MethodCaller(self._provider, "__getitem__", item)
def _provide(self, args, kwargs):
return self._provider(*args, **kwargs)
class MyContainer(containers.DeclarativeContainer):
b = MySingleton(B)
a4 = providers.Singleton(
A,
b.provided.val,
b.provided.val_method.call(),
b.provided.vals[0],
b.provided[0],
b.provided,
)
if __name__ == '__main__':
container = MyContainer()
b = container.b()
print(b)
a4 = container.a4()
print(a4)
assert a4.b is b |
Work is in progress. I did some changes to make attribute getter, item getter and method caller to work in tandem. It's funny to play with it: class MyContainer(containers.DeclarativeContainer):
b = providers.Singleton(B)
d = providers.Object(
{
'a': {
'b': {
'c1': 10,
'c2': lambda arg: {'arg': arg}
},
},
},
)
a4 = providers.Singleton(
A,
b.provided.val,
b.provided.val_method.call(),
b.provided.vals[0],
b.provided[0],
b.provided,
)
l = providers.List(
d.provided['a']['b']['c1'],
d.provided['a']['b']['c2'].call(22)['arg'],
d.provided['a']['b']['c2'].call(a4)['arg'],
d.provided['a']['b']['c2'].call(a4)['arg'].b_val,
d.provided['a']['b']['c2'].call(a4)['arg'].get_b2_val.call(),
) I have it working. Productizing it will take some time. I would like to get all this well covered with tests. |
@rmk135 Glad I could contribute some idea. I never heard about TRIZ, but it sounds like an interesting read!
Great, let me know if I can contribute something. I believe the feature will make dependency injector extremely flexible, lowering the barrier for people adopting it. |
The feature is published in the @JarnoRFB , I've added you to the list of contributors that is distributed with each copy of the Dependency Injector - https://github.com/ets-labs/python-dependency-injector/blob/master/CONTRIBUTORS.rst I've also updated the changelog at http://python-dependency-injector.ets-labs.org/main/changelog.html stating your design contribution. That's great. Thank you. There is a docs page for the new feature - http://python-dependency-injector.ets-labs.org/providers/provided_instance.html. As of contribution, you're very welcome. Look at #282 as an example. Ignore the |
@rmk135 I am amazed by the speed you implemented this. Thanks a lot! |
Hi, I recently needed to access an attribute of a provided object for constructing a new object. A minimal example would look like this
naively I tried to do
which does not work, because of
AttributeError: 'dependency_injector.providers.Singleton' object has no attribute 'val'
.I was able to work around this, by using
However, I thought that it would be nice if
dependency_injector
would provide this behavior out of the box.My attempts at implementing a provider that has this behavior look like
However, this fails with
TypeError: __init__() takes at least 1 positional argument (0 given)
because of some copying, that I can not really interpret.I would be interested in hearing thoughts on this or if there are other ways to achieve this kind of attribute access.
The text was updated successfully, but these errors were encountered: