Coder Social home page Coder Social logo

kink's People

Contributors

dependabot[bot] avatar dkraczkowski avatar dosisod avatar ijessen-mitll avatar metaperl avatar patrykmor avatar szymon6927 avatar tfriedel avatar tubaman avatar wetgi 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  avatar  avatar  avatar  avatar

kink's Issues

Container `__getitem__` type hinting improvements

The following overloads seem to fix typing for me in PyCharm 2023.2.

from kink import di

di[bytes].|    # gives auto-completion for split, decode, join, etc..
from typing import Any, Dict, Type, Union, Callable, List, TypeVar, overload

T = TypeVar('T')

class Container:

    @overload
    def __getitem__(self, key: str) -> Any:
        ...

    @overload
    def __getitem__(self, key: Type[T]) -> T:
        ...

    def __getitem__(self, key):
        ...same code as before...

Registering a service with value `None` will trigger ServiceError("Service ... is not regisitered") on access

Reproduce

from kink import di

di['a'] = None
di['a']  # <-- Raise Service Error

Reason

Object None is used to tell if a service is registered or not. This is why a service registered with the value None is treated as not registered.
Container.__getitem__
https://github.com/kodemore/kink/blob/master/kink/container.py#L34-L35
https://github.com/kodemore/kink/blob/master/kink/container.py#L40-L41
Container._get
https://github.com/kodemore/kink/blob/master/kink/container.py#L55-L56

Proposal

Define another sentinel object for this purpose.

MISSING = object()

# in `Container.__getitem__`
if service is not MISSING:
    return service

# in `Container._get`
if key not in self._services:
    return MISSING

[QUESTION] How kink works with async?

Hi!

First of all, thanks for such simple and useful DI!

In readme it is said that kink support "async with asyncio" however I don't see any examples of such functionality. Is it possible to create some fastapi-like async dependency?

Concrete case (sqlalchemy's AsyncSesssion):

async def get_session() -> AsyncSession:
    async_session = sessionmaker(
        engine, class_=AsyncSession, expire_on_commit=False
    )
    async with async_session() as session:
        yield session

I tried I few workaround, but all of them just does not work, as kink understands (as it seems to me) only lambda-based factory functions.

Using Optional type is not working

Hi in this example, autowiring is not working:

class IPublisher(abcABC):
    @abc.abstractmethod
    def publish(
        self,
    ) -> None:
        ...

@inject(alias=IPublisher)
class Publisher(IPublisher):
    def publish(
        self,
    ) -> None:
        print("publish")

@inject
class EntitiesSubscriber:
    def __init__(
        self,
        publisher: Optional[IPublisher] = None,
    ) -> None:
        ...

When in subscriber is services typed as optional: publisher: Optional[IPublisher] = None autowiring is not working
When it is changed to List[IPublisher] autowiring will inject all services and also when Optional type is removed: publisher: IPublisher = None service is injected also but this is leading to invalid typing

[mypy] Missing positional argument "x" in call to "C"

I am getting mypy errors when using the inject decorator on classes:

# file.py

from kink import di, inject

di["x"] = 42

@inject
class C:
    def __init__(self, x: int) -> None:
        print(x)


c = C()

Running mypy with:

$ mypy file.py
file.py:13: error: Missing positional argument "x" in call to "C"
Found 1 error in 1 file (checked 1 source file)

Version info:

$ pip freeze
kink==0.6.2
mypy==0.910
mypy-extensions==0.4.3
toml==0.10.2
typing-extensions==3.10.0.2

$ python --version
Python 3.9.7

The only workaround I can see is this:

class C:
    def __init__(self, x: int = di["x"]) -> None:

Which means the inject decorator isn't needed.

Inject multiple implementations of an interface with Type based injection

A very useful feature when using Type based injection would be the ability to inject all the implementations for a given interface as a list.

Here is an example of this same feature from the Java Spring documentation:
https://docs.spring.io/spring-framework/docs/4.3.12.RELEASE/spring-framework-reference/htmlsingle/#beans-autowired-annotation
Screenshot 2021-06-09 at 15 03 56

The way it would be used would be as follows:

class IMyService:
    def my_function(self, val: str) -> str:
        ...


@inject(alias=IMyService)
class FirstService(IMyService):
    def my_function(self, val: str) -> str:
        return f'First {val}'


@inject(alias=IMyService)
class SecondService(IMyService):
    def my_function(self, val: str) -> str:
        return f'Second {val}'

@inject
def list_vals(services: List[IMyService]) -> List[str]:
    return [service.my_function() for service in services]

In the snippet above, the parameter services of list_vals would receive the objects of both classes implementing IMyService.

Bug on files with `annotations` import

Hello! Great project you got going in here - very useful.
I think I may have found a bug, @inject seems to fail when annotations is imported. This is a mini example of what I have found.

from __future__ import annotations
from kink import inject

@inject
class ClassB(object):
    def __init__(self) -> None:
        pass

@inject
class ClassA(object):
    def __init__(self, class_b: ClassB):
        self._class_b = class_b

example = ClassA()

Running the above blows up:

Traceback (most recent call last):
  File "example.py", line 16, in <module>
    example = ClassA()
  File "/home/x/.local/lib/python3.8/site-packages/kink/inject.py", line 119, in _decorated
    all_kwargs = _resolve_kwargs(args, kwargs)
  File "/home/x/.local/lib/python3.8/site-packages/kink/inject.py", line 103, in _resolve_kwargs
    raise ExecutionError(
kink.errors.execution_error.ExecutionError: Cannot execute function without required parameters. Did you forget to bind the following parameters: `class_b`?

But if the from __future__ import annotations import is removed everything works as expected. I tried the above with both python3.8 and python3.9 and I'm on Ubuntu 20.04 .

Not working with typing-extensions 4.1.1

We need to use that version but is not compatible with the current kink version. I tried to make a PR but I'm getting a 403 when I try to update my new branch.

Cheers.

Potential issue where constructor is not defined

I'm not sure if this is an issue with my syntax/configuration or if it might be a small bug.

I am getting an error when I try to inject an implementation that does not define an __init__ method:

kink.errors.execution_error.ExecutionError: Cannot execute function without required parameters. Did you forget to bind the following parameters: `args`, `kwargs`?

It doesn't seem to be a big issue because after experimenting a bit, it seems that simply defining an init method that does nothing seems to make the error go away, such as:

def __init__(self):
    pass

I've created a test case that demonstrates the problem as I see it:

from typing import Protocol

from kink import di
from kink import inject


class ExampleProtocol(Protocol):
    def run(self, a: int, b: int) -> int:
        ...


@inject(alias=ExampleProtocol)
class ImplementationNoConstructor(ExampleProtocol):
    def run(self, a: int, b: int) -> int:
        return a + b


@inject(alias=ExampleProtocol)
class ImplementationWithConstructor(ExampleProtocol):
    def __init__(self):
        pass

    def run(self, a: int, b: int) -> int:
        return a * b


@inject
class CompositionExample:
    def __init__(self, example: ExampleProtocol):
        self._example: ExampleProtocol = example

    def use_it(self, a: int, b: int) -> int:
        return self._example.run(a=a, b=b)


def test_that_auto_wiring_works_with_constructor():
    """
    This one works - I think because ImplementationWithConstructor.__init__ is defined
    """
    di[ExampleProtocol] = ImplementationWithConstructor()
    composition_example = CompositionExample()
    assert composition_example.use_it(a=2, b=3) == 6


def test_that_auto_wiring_works_without_constructor():
    """
    This one doesn't work - I think because there's no __init__ defined on ImplementationNoConstructor
    """
    di[ExampleProtocol] = ImplementationNoConstructor()
    composition_example = CompositionExample()
    assert composition_example.use_it(a=2, b=3) == 5

QUESTION. Does kink do partial injections?

Hey, very nice and lighweight di library!

I'm trying to inject a positional argument to a descendent decorator while it has more positionals.
The piece of code, it's more likely incorrent, but whatever:

def command(name, description, *scopes):
    
    def wrapped_func(func):
        
        async def wrapped(*args, **kwargs):
            cmd = BotCommand(name, description)
            for scope in scopes:
                SCOPES[scope].append(cmd)
            
            di[BotCommand] = cmd
            result = await func(*args, **kwargs)
            return result
        
        return wrapped
    
    return wrapped_func


@inject
def handler(bot_cmd: BotCommand, *handler_args, handler_class=CommandHandler, **handler_params):
    
    def wrapped_func(func):
        _handler = handler_class(bot_cmd.command, func, *handler_args, **handler_params)
        APP_HANDLERS.append(_handler)

        async def wrapped(*args, **kwargs):
            result = await func(*args, **kwargs)
            return result

        return wrapped

    return wrapped_func


@command('stop', 'Stop a container', BotCommandScopeChat)
@handler(StartStopArgs, filters=filters.User(user_id=1))
async def stop_container(*args):
    print(args)

For now, an error is raised:

raise ExecutionError(
kink.errors.execution_error.ExecutionError: Cannot execute function without required parameters. Did you forget to bind the following parameters: `handler_args`, `handler_params` inside the service `<function handler at 0x7fe16b1b8400>`?

To be honest, i expected that di only injectes the bot_cmd argument and leave the others handler_args, handler_params as is, not check them for beign in a container.
Could it be defined to make a partial check?

Error with @inject typing and mypy

This code works with mypy:

class MyClass:
    pass


di[MyClass] = MyClass()


async def my_async_func(my_class: MyClass) -> str:
    return MyClass.__class__.__name__


async def calling_func() -> str:
    return await my_async_func(di[MyClass])

While this code results in an error:

class MyClass:
    pass


di[MyClass] = MyClass()


@inject
async def my_async_func(my_class: MyClass) -> str:
    return MyClass.__class__.__name__


async def calling_func() -> str:
    return await my_async_func()

mypy error: error: Incompatible types in "await" (actual type "Union[Any, Callable[..., Any]]", expected type "Awaitable[Any]")

Aside from ignoring the calling line, is there any way to make mypy happy with the type returned from the @Inject decorator?

Missing example of using inject as a function on 3rd party code

It is likely not obvious to many developers that kink's inject decorator function can be used as a regular function (not as a decorator) to make 3rd party classes injectable without needing to modify the 3rd party code at all.

I wrote this bit of code to demonstrate how to do it:

from kink import di, inject


class Some3rdPartyClass:  # notice how the 3rd party class code does not need to be modified
    def __init__(self, captains_name: str) -> None:
        self.value = captains_name


def approach(num: int):
    if num == 1:
        # each injection request for Some3rdPartyClass will get the same instance (singleton)
        inject(Some3rdPartyClass)  # registers this class as injectable and takes care of its parameters
        di["captains_name"] = "Cpt. Obvious"
    elif num == 2:
        # each injection request for Some3rdPartyClass will get the same instance (singleton)
        di["captains_name"] = "Cpt. Obvious"
        di[Some3rdPartyClass] = Some3rdPartyClass(di["captains_name"])
    elif num == 3:
        # each injection request for Some3rdPartyClass will get a different instance
        di.factories[Some3rdPartyClass] = lambda dic: Some3rdPartyClass(dic["captains_name"])
        di["captains_name"] = "Cpt. Obvious"
    else:
        raise ValueError("invalid selection")


approach(3)
print(di[Some3rdPartyClass].value)

Perhaps something like this could be included in the docs to help others recognise this possibility as well.

Version 0.6.4 broke mypy linting for `@inject` decorator

After new release I'm not able pass typing check on all my projects :(

@inject(
    alias=IClient,
    bind={
        "event_dispatcher": EventDispatcher,
        "consumer": Consumer,
    },
)
class Client(IClient):
    def __init__(
        self,
        event_dispatcher: Optional[EventDispatcher] = None,
        consumer: Optional[Consumer] = None,
    ) -> None: ...

is not valid anymore :(

cannot alias a factory service with inject

inject cannot find services which are both aliased and factoried. For example:

class Repository: ...

@inject(alias=Repository, use_factory=True)
class PerInstanceRepository(Repository): ...

@inject
class Service:
   def __init__(self, repository: Repository): ...

Container.__getitem__ looks for factory services before resolving aliases. However, factories are not referenced again after aliases are resolved.

Automatically register base class(es) as aliases when using `@inject`

Hi, thanks for making this awesome library (I found an article on Google recommending this library).

My question was: The documentation mentions for service aliasing that:

When you register a service with @Inject decorator you can attach your own alias name

class IUserRepository(Protocol):
   ...

@inject(alias=IUserRepository)
class UserRepository:
    ...

I always explicitly define relationships between my "interfaces" and their implementations through base classes so that the IDE does static type-checking to verify that the subclass has implemented everything from the superclass. (It's also clear to others that the class is intended to implement all the methods of the interface). Simple example:

class IUserService(ABC):
    @abstractmethod
    def get_username(self, user_id: str) -> str:
        pass

@inject(alias=IUserService)
class UserService(IUserService):
    def get_username(self, user_id: str) -> str:
        return self.db.execute_query(blablabla)

The official Python documentation also shows a similar example with that recommendation:

Explicitly including a protocol as a base class is also a way of documenting that your class implements a particular protocol, and it forces mypy to verify that your class implementation is actually compatible with the protocol.

Since inheritance already implies by definition that UserService is an instance of IUserService, having to manually define that IUserService is an alias of UserService within the @inject annotation is redundant. (ie it's already clear to a Python developer that UserService is an instance of IUserService, so when I want to get all the implementations of IUserService, I should expect to see UserService in that list by definition).

(This is also the default behavior in other major di frameworks like SpringBoot in Java, which saves a lot of time).

Thus, I wanted to request that can we automatically register base class(es) as aliases when using @inject on a class?

(Searching online briefly, it seems we can use inspect.getmro(B) or cls.__bases__ to get the base classes to auto-register as aliases at runtime)

autocomplete not working for injected classes

Thanks for making this nice minimal library! I'm giving it a try for a project currently.
One thing I noticed, when I use classes that are decorated with @inject, the autocompletion or display of signature parameters in jupyter notebooks doesn't work.
I didn't dive into the source, but maybe you can use some of the suggestions mentioned here:
https://hynek.me/articles/decorators/

Can't use non-lambda on-demand service creation

Consider the following code:

from kink import di, inject, Container

def on_demand_1(di: Container):
    return "foo"

on_demand_2 = lambda di: "bar"

di['on_demand_1'] = on_demand_1
di['on_demand_2'] = on_demand_2

@inject
def test(on_demand_1, on_demand_2):
    print(f"{on_demand_1=}")
    print(f"{on_demand_2=}")

test()

I see the following output:

on_demand_1=<function on_demand_1 at 0x102a629d0>
on_demand_2='bar'

Is there any clear reason why non-lambda functions don't work for on-demand service creation? Would it be difficult to support such a pattern?

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.