Giter Site home page Giter Site logo

roo-oliv / injectable Goto Github PK

View Code? Open in Web Editor NEW
107.0 5.0 8.0 909 KB

Python Dependency Injection for Humans™

Home Page: https://injectable.readthedocs.io

License: MIT License

Python 97.49% Makefile 2.51%
python dependency-injection ioc lazy-evaluation circular-dependencies micro-framework autowired autowiring injection for-humans

injectable's People

Contributors

craigminihan avatar mt3o avatar roo-oliv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

injectable's Issues

ImportError due to changes in 3.4.3

On an existing codebase I am seeing ImportErrors when upgrading from 3.4.1 to latest.

The error I am seeing is: ImportError: attempted relative import with no known parent package

Prior to 3.4.3 all modules load successfully.

I've not had time to put an example together showing the exact error case yet. I'll follow up with this later.

Stack:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/craig/code/XXX.py", line 7, in <module>
    injectable.load_injection_container(search_path=os.getenv("PYTHONPATH"))
  File "/home/craig/.local/lib/python3.6/site-packages/injectable/container/load_injection_container.py", line 43, in load_injection_container
    search_path, default_namespace or DEFAULT_NAMESPACE
  File "/home/craig/.local/lib/python3.6/site-packages/injectable/container/injection_container.py", line 158, in load_dependencies_from
    run_path(file.path)
  File "/usr/lib/python3.6/runpy.py", line 263, in run_path
    pkg_name=pkg_name, script_name=fname)
  File "/usr/lib/python3.6/runpy.py", line 96, in _run_module_code
    mod_name, mod_spec, pkg_name, script_name)
  File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "MYFILE", line 3, in <module>
    from .MYCLASS
ImportError: attempted relative import with no known parent package

[Bug] inject method signature accepts T instead of Type[T]

Current situation:

T = TypeVar("T")

def inject(dependency: Union[T, str], ...) -> T
inject(A) # claims to return A not something of type A

Example:
grafik

Correct situation:

T = TypeVar("T")

def inject(dependency: Union[Type[T], str], ...) -> T
inject(A) # now claims to return an instance of A

I'm not an expert when it comes to typing, but this is what i think causes it. Also maybe this is an issue with intellijs type system, but I don't have any other type checkers rn.

By submiting an issue I accept this project's
Code of Conduct. Any issue out of
these guidelines can be deleted at any moment without further explanation.

Making typing.Type injectable.

Introduction

I am currently into an issue where i need the Type of all @injectable objects. These objects have a common supertype, but no empty __init__, so I can't just use Autowired(List[BaseType]). Concrete, this is a database project, and the model classes need to be registered so they can be created in the database.

This is kinda far fetched, and i could definitely see why this would be rejected, as database models are kind of the only usecase i coud think of, and it is kind of narrow.

Feature Request

Allowing the Autowired(Type[BaseType]) to pick up an injectable subclass of a BaseType, or Autowired(List[Type[BaseType]]) to get all injectable subclasses. Maybe this could be indicated by @injectable(type=True).

By submiting an issue I accept this project's
Code of Conduct. Any issue out of
these guidelines can be deleted at any moment without further explanation.

[Feature Request] Manually supplying injectables

Right now it seems impossible to directly supply an injectable instance, except for providing a factory method like so:

a = A()

@injectable_factory(A, singleton=True)
def a_injector():
    return a

The alternative would be to move the initialization into the factory, however, sometimes the initialization will run independent from injection discovery, so this isn't applicable. Examples for this would be flask projects with multiple blueprints (i am aware of flask.current_app, but this doesn't always work (e.g. extensions)) and i think providing a single injectable directly would be generally useful, tho i can see why it might go against design thoughts behind injections.

By submiting an issue I accept this project's
Code of Conduct. Any issue out of
these guidelines can be deleted at any moment without further explanation.

First-class support for testing

Supplying fake/mocked/stubbed injectables should be really easy.

An example feature would be to introduce a helper fixture for pytest:

@pytest.fixture()
def given_injectable():
    originals: Dict[Union[type, str], Tuple[Set[Injectable], bool]] = {}

    def register(constructor: callable, klass=None, qualifier=None, propagate=False):
        if not klass and not qualifier:
            raise ValueError("params 'klass' and 'qualifier' cannot be both None")
        if propagate and not klass:
            raise ValueError("param 'klass' must be set when 'propagate' is True")
        for dependency in klass, qualifier:
            if not dependency or dependency in originals:
                continue
            originals[dependency] = clear_injectables(dependency), propagate
        register_injectables(
            {Injectable(constructor)}, klass, qualifier, propagate=propagate
        )

    yield register

    for dependency, (injectables, propagate) in originals.items():
        clear_injectables(dependency)
        if isinstance(dependency, str):
            register_injectables(injectables, qualifier=dependency, propagate=propagate)
        else:
            register_injectables(injectables, klass=dependency, propagate=propagate)

Using named args breaks injectable

Injectable 3.4.0 has a bug that attempts to use positional args after named args passed by the caller. The following piece of code reproduces the issue:

@autowired
def bar(qux, foo: Autowired(Foo)):
    ...

bar(qux="QUX")

Running it with injectable 3.4.0 and Python 3.6.8 will raise TypeError: bar() got multiple values for argument 'qux'.

This issue was originally reported by @craigminihan in #14

Optional injection doesn't work when there are the namespce is empty

Injectable 3.4.1 raises a KeyError for the namespace specified by an optional injection if there are no injectables registered in it. The expected behavior would be for it to inject None instead of raising an error. The following piece of code reproduces the issue:

@autowired
def a(x: Autowired(Optional["anything"])):
    ...

Running it with injectable 3.4.1 and Python 3.6.8 will raise KeyError: 'DEFAULT_NAMESPACE'.

No context is created when no injectables are found, attempt to put object - fails #triage @bug

When we create a new context with load_injection_container() and there are no injectable objects found, no namespace is being created in injection_container.py. When we attempt to programmatically add an object into the context - it fails - because no default context (namespace) has been created.

Code to reproduce:

from injectable import Injectable, load_injection_container
from injectable.testing import register_injectables


class Sample():
    pass


def fail():
    sample = Sample()
    mocked_injectable = Injectable(lambda: sample)
    register_injectables({mocked_injectable}, Sample)
    print("it's broken")


def run_example():
    load_injection_container()
    fail()


if __name__ == "__main__":
    run_example()

Response:

  File "sandbox/fail.py", line 12, in fail
    register_injectables({mocked_injectable}, Sample)
  File "\.virtualenv\lib\site-packages\injectable\testing\register_injectables_util.py", line 54, in register_injectables
    namespace = InjectionContainer.NAMESPACES[namespace or DEFAULT_NAMESPACE]
KeyError: 'DEFAULT_NAMESPACE'

In this case - when we try to register_injectables, we see KeyError on attempt to get default namespace - when there is none.

How to fix:

When container fails to find any injection candidates - it should create empty default namespace anyway.

Comply with PEP-593 and support typing.Annotated

PEP-593 defines the a standard for annotation metadata other than type hinting through the use of typing.Annotated. This framework should comply with this recommendation and give full support to typing.Annotated.

This was originally brought to attention by @Euraxluo in #107 (comment)

Initial Considerations

Currently Autowired(T) already works well with most type checkers and linters since it evaluates to T but this is not in line with the recommendation from PEP-593 to leave annotations primary to type hinting and other metadata to extras.

We should decide how best to incorporate these changes in Injectable. I intend to lay down some possible paths we can go from here in this issue thread so anyone can share their thoughts if any and then eventually I'll commit to an implementation.

While typing.Annotated was introduced in Python 3.9, this frameworks currently supports all Python versions since 3.6. So we should think in a way that will deliver the best possible experience for all possible users and use cases of Injectable.

Current State

When Python version < 3.9 then this is the only possible way to use Injectable:

@autowired
def foo(service: Autowired(Service): ...

When typing.Annotated is available (Python version >= 3.9), then this is also possible:

@autowired
def foo(service: Annotated[Autowired(Service), ...]): ...

Therefore, since Python 3.9 one can probably use other libs which rely on annotations and fully support PEP-593 without undesired interactions or further problems with the use of Injectable.

Proposals

Enable `@autowired` decorator to be called without parenthesis

When one doesn't want to pass in any parameters into @autowired decorator they can't symple do:

@autowired
def foo(...)

Instead they are required to use empty parenthesis for it to work:

@autowired()
def foo(...)

It would be best to omit the parenthesis when no arguments will be used.

Autowiring should be done without actually importing the dependency's class

Autowiring should be able to resolve dependencies from a global scope without requiring the class specification:

This is how one would autowire today:

# application.models.model.py
class Model:
    def __init__():
        self.foo = 'foo'

# application.services.service.py
class Service:
    def __init__():
        self.bar = 'bar'

# application.qux.qux.py
from injectable import autowired
from application.models.model import Model
from application.services.service import Service

class Qux:
    @autowired
    def __init__(self, *, model: Model, service: Service):
        self.model = model
        self.service = service

This is how it should be:

# application.models.model.py
from injectable import injectable

class Model:
    @injectable
    def __init__():
        self.foo = 'foo'

# application.services.service.py
from injectable import injectable

class Service:
    @injectable
    def __init__():
        self.bar = 'bar'

# application.qux.qux.py
from injectable import autowired
from injectable import inject

class Qux:
    @autowired
    def __init__(self, *, model = inject('Model'), service = inject('Service')):
        self.model = model
        self.service = service

Evaluate the use of code scanning tools other than SonarQube

@chsatyap opened Pull Request #23 to suggest the use of DeepSource.

As part of the new tools adoption process evaluation on the actual need for such a tool must be performed and should consider:

  • alternatives;
  • ease of usage;
  • ease of maintenance;
  • added benefit;
  • community support;
  • reliability;

Official Support for Python 3.9

Injectable shall officially support Python 3.9.

Most notably we should:

  • Support type hinting generics in standard collections introduced by PEP-585;
  • Explore the relaxed grammar restrictions on decorators introduced by PEP-614

Lazy type class

Currently, when not using the parameter lazy of @autowired, Inejctable provides a lazy function to pass in your dependencies' type annotations and make them lazy.

Under the hood it will just return a function that returns the dependency type and has an attribute lazy set to True. This won't break IDEs' (eg PyCharm) code completion.

So we have this syntax today:

@autowired()
__init__(*args, *, controller: lazy(Controller)):
    ...

It would be best to have a subscriptable Lazy type which doesn't break IDEs' code completion.

The intended syntax would be this:

@autowired()
__init__(*args, *, controller: Lazy[Controller]):
    ...

Support for async injectables

It's currently not ideal to use this library together with Python's async/await.

We should support asynchronous constructors and factories.

Async constructors are also a bigger problem since one cannot declare an async __init__ method without bending some snakes. This calls for ways to construct and/or setup an injectable other than relying solely on __init__.

Autowired(List[...]) does not work with qualifiers

This script reproduces the bug:

from typing import List
from injectable import injectable, autowired, Autowired, InjectionContainer

@injectable(qualifier="foo")
class Foo:
    pass

@autowired
def test(foo: Autowired(List["foo"])):
    assert foo is not None
    assert len(foo) == 1

if __name__ == "__main__":
    InjectionContainer.load()
    test()

The returned error is quite cryptic:

AttributeError: '_ForwardRef' object has no attribute '__qualname__'

Injectable fails to resolve entangled imports

Injectable 3.4.2 raises an ImportError when a file imports its module's __init__ file and the __init__ file in turn imports that file. This is a problem with Injectable because Python import system is able to deal with these scenarios and they're not actually circular imports as someone would think. The following minimum sample project reproduces the issue:

project
│   main.py
│
└───test_module
│   │   __init__.py
│   │
│   └───foo_module
│   |   │   __init__.py
│   |   │   foo.py
│   │
│   └───utils
│       │   __init__.py
│       │   some_util.py
# ./main.py
from injectable import autowired, Autowired, load_injection_container
from injectable.testing import reset_injection_container


@autowired
def test(foo: Autowired("foo")):
    ...


if __name__ == "__main__":
    reset_injection_container()
    load_injection_container()
    test()
# ./test_module/__init__.py
from project.test_module.foo_module import Foo

__all__ = ["Foo"]
# ./test_module/foo_module/__init__.py
from project.test_module.foo_module.foo import Foo

__all__ = ["Foo"]
# ./test_module/foo_module/foo.py
from injectable import injectable
from project.test_module.utils import some_util


@injectable(qualifier="foo")
class Foo:
    def __init__(self):
        some_util.some_util()
# ./test_module/utils/__init__.py
# EMPTY
# ./test_module/utils/some_util.py
def some_util():
    ...

Running it with injectable 3.4.2 and Python 3.6.8 will raise:

ImportError: cannot import name 'Foo' from 'project.test_module.foo_module.foo' (/project/test_module/foo_module/foo.py)

Original function's signature overwrite

When a function decorated with @autowired is inspected to get it's signature the original function's signature with required kwargs should be overwritten by a signature exposing injectables as optional kwargs.

import inspect

@autowired
def foo(a: Autowired("A")):
    print(a)

inspect.signature(foo)
# <Signature (a: <injectable.autowiring.autowired_type._Autowired object at 0x7ff88eea53a0>)>

@autowired not overwriting the original signature may cause checks of uses vs signature to fail.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.