proofit404 / dependencies

Constructor injection designed with OOP in mind.
https://proofit404.github.io/dependencies/
BSD 2-Clause "Simplified" License
361 stars 18 forks source link

workaround to avoid checking types on dependencies? #533

Open s22chan opened 2 years ago

s22chan commented 2 years ago

I need to use some virtual proxies for some complex classes and https://github.com/ionelmc/python-lazy-object-proxy provides a no effort pluggable solution.

Unfortunately, https://github.com/proofit404/dependencies/blob/697f97c925419ad034efe7b416611f51fc80f97d/src/_dependencies/objects/classes.py#L36-L38 triggers the instantiation.

I was wondering if there's a way to get around this, or will I have to make custom proxies for each class?

Great library by the way, really appreciate how much effort you're putting into supporting it.

proofit404 commented 2 years ago

Good evening,

Could you provide full code example which cause en error with library you mention?

I would try to figure out how to fix it.

Best regards, Artem.

s22chan commented 2 years ago
from dependencies import Injector
from lazy_object_proxy import Proxy

class Outer:
   def __init__(self, inner):
       self.inner = inner

class Inner:
   def __init__(self):
     print("inner")

class Root(Injector):
    outer = Outer
    inner = Inner

Ideally, Root.outer would assign the inner proxy to outer, which will defer any resource allocation until the outer.inner attribute is actually used; but calling isinstance(inner, _IsScope) calls https://github.com/ionelmc/python-lazy-object-proxy/blob/03003b012feef472b4bb54b971a8f4782a41f93f/src/lazy_object_proxy/slots.py#L128 which instantiates at https://github.com/ionelmc/python-lazy-object-proxy/blob/03003b012feef472b4bb54b971a8f4782a41f93f/src/lazy_object_proxy/slots.py#L106.

I understand that dependencies can't be expected to support all these little hacks, so unless there's an easy solution, I'll look into other libraries/custom code to make virtual proxies. Thanks again!

proofit404 commented 2 years ago

Good morning,

I did some initial testing for lazy_object_proxy library. Generally, I'm interested in support of such libraries.

What I have at that time:

>>> from dependencies import Injector
>>> from lazy_object_proxy import Proxy
>>> class Inner:
...     def __init__(self, x):
...         print('x', x)
...
>>> class Container(Injector):
...     a = Proxy
...     factory = Inner
...     x = 1
...
Traceback (most recent call last):
  ...
_dependencies.exceptions.DependencyError: Proxy.__init__ have arbitrary argument list and keyword arguments

Due to implementation on C layer.

This could be solved in the future with shield object #487

proofit404 commented 2 years ago

Speaking about early instantiation, I could confirm it :cry:

>>> from lazy_object_proxy import Proxy
>>> from dependencies import Injector
>>> class Inner:
...     def __init__(self, x):
...         print('x', x)
...
>>> class Container(Injector):
...     a = Proxy
...     factory = Inner
...     x = 1
...
>>> Container.a
x 1
<Proxy at 0x7fbdfb7a5840 with factory <__main__.Inner object at 0x7fbdfb7ced10>>

In that case I see two approaches to workaround this problem.

First of all, you could try to wrap Injector itself into lazy proxy.

>>> class Container(Injector):
...     inner = Inner
...     x = 1
...
>>> Proxy(lambda: Container.inner)
<Proxy at 0x7fbdfb4b7f40 with factory <function <lambda> at 0x7fbdfb67e950>>
>>> str(Proxy(lambda: Container.inner))
x 1
'<__main__.Inner object at 0x7fbdfb7ced10>'

Or better rewrite your classes to avoid resource allocation inside constructor.

class Inner:
    def __init__(self, socket):
        ...
    @classmethod
    def connect(cls, host, port):
        socket = make_connection(host, port)
        return cls(socket)

I would recommend this talk https://www.youtube.com/watch?v=FThx_Jk24Rc

What do you think?

s22chan commented 2 years ago

sorry I realize I didn't complete the example:

class Root(Injector):
    outer = Outer
    @value
    def inner():
        return Proxy(Inner)

is how I would use it, so I would've liked the wiring of the proxy to be defined by the Injector. Not sure how I would use a Proxied injector in this case to supply the argument to Outer

Yeah I totally agree that the constructors should be lightweight, but I'm refactoring a rather large code-base and would like to be doing this baby steps at a time. Thanks for the video talk.

proofit404 commented 2 years ago

Indeed, Proxy(Injector()) could be used only to wrap main injector. This solution does not work with nested Injectors.

I just tried to reproduce example you provide:

from dependencies import Injector, value
from lazy_object_proxy import Proxy

class Inner:
    def __init__(self, x):
        print("inner")
        self.x = x

    def do(self):
        return self.x

class Outer:
    def __init__(self, inner):
        print("outer")
        self.inner = inner

    def do(self):
        return self.inner.do()

class Container(Injector):
    outer = Outer

    @value
    def inner(x):
        return Proxy(lambda: Inner(x))

    x = 1
>>> o = Container.outer
outer

>>> o.do()
inner
1

Looks like it kinda works :thinking:

s22chan commented 2 years ago

odd I'm not sure why I get a different result running your sample. I'll debug it later. Thanks again for the great support:

In [2]: o = Container.outer
inner
outer
proofit404 commented 2 years ago

Quick suggestion: try both ipython and python -i consoles. It could introduce additional magic.

s22chan commented 2 years ago

I've tried both, it still outputs inner. I've also tried python 3.7..3.9. The only time I don't see this is if I rollback dependencies to 6.0.1 (from 7.1.7)

I'll update it if I can see what's going on later (I'm also using lazy-objects-proxy==1.7.1)

proofit404 commented 2 years ago

That's because I'm an idiot.

My system has python 3.10 by default, most recent dependencies release specified 3.9 as supported, so pip decided to install 3.0.0 version as most compatible.

proofit404 commented 2 years ago

Sorry it takes me too long to get back to this issue.

I could confirm Proxy object does eager initialization in the example I post previously.

>>> from dependencies import Injector, value
>>> from lazy_object_proxy import Proxy
>>> class Inner:
...     def __init__(self, x):
...         print("inner")
...         self.x = x
...     def do(self):
...         return self.x
...
>>> class Outer:
...     def __init__(self, inner):
...         print("outer")
...         self.inner = inner
...     def do(self):
...         return self.inner.do()
...
>>> class Container(Injector):
...     outer = Outer
...     @value
...     def inner(x):
...         return Proxy(lambda: Inner(x))
...     x = 1
...
>>> o = Container.outer
inner
outer
>>> o.do()
1
>>>
dependencies==7.1.7
lazy-object-proxy==1.7.1
proofit404 commented 2 years ago

I found the way to workaround isinstance check you mention it the very first message.

from dependencies import Injector, value
from lazy_object_proxy import Proxy as _Proxy

class Inner:
    def __init__(self, x):
        print("inner")
        self.x = x

    def do(self):
        return self.x

class Outer:
    def __init__(self, inner):
        print("outer")
        self.inner = inner

    def do(self):
        return self.inner.do()

class Proxy(_Proxy):
    @property
    def __class__(self):
        return Proxy

class Container(Injector):
    outer = Outer

    @value
    def inner(x):
        return Proxy(lambda: Inner(x))

    x = 1
>>> from t import Container
>>> o = Container.outer
outer
>>> o.do()
inner
1
>>>

I'll include this example into documentation later today, since it's not obvious how to solve it.

s22chan commented 2 years ago

Thanks Artem, that's amazing!