import inspect

class ContainerDefinition:
    __slots__ = 'val',

    def __init__(self, val):
        self.val = val

class Container:
    def __init__(self):
        self._map = {}
        self._cache = {}

        for key, fn in self.__class__.__dict__.items():
            if not isinstance(fn, ContainerDefinition):
                continue

            try:
                spec = inspect.getfullargspec(fn.val)
                if len(spec.annotations) != len(spec.args):
                    raise TypeError('%s does not have all arguments annotated' % key)

                if spec.varargs or spec.varkw:
                    raise TypeError('%s must not use variadic arguments' % key)

                return_type = spec.annotations['return']
                self._map[return_type] = fn.val, spec
            except KeyError as e:
                raise TypeError('%s does not have annotated return value' % key) from e

    def definition():
        def wrapper(fn):
            return ContainerDefinition(fn)
        return wrapper

    def _get_method_args(self, fn, spec):
        if len(spec.args) == 0:
            return []

        args = []
        diff = len(spec.args) - (1 + 0 if not spec.defaults else len(spec.defaults))

        for i, arg in enumerate(spec.args):
            if i == 0: # "self"
                continue

            if arg in spec.annotations:
                argtype = spec.annotations[arg]
                if not inspect.isclass(argtype):
                    raise TypeError('Argument %s of %s() is not a class' % (
                        arg, fn.__qualname__
                    ))

                append = self._get_class(argtype)
            elif spec.defaults and i >= diff:
                append = spec.defaults[diff - i]
            else:
                raise TypeError('Argument %s of %s() neither has typehint nor a default value' % (
                    arg, fn.__qualname__
                ))

            args.append(append)
        return args

    def _get_class(self, cls):
        try:
            return self._cache[cls]
        except KeyError:
            pass

        try:
            fn = self._map[cls]
            args = self._get_method_args(fn[0], fn[1])
            rv = self._cache[cls] = fn[0](self, *args)
            return rv
        except KeyError:
            pass

        for type, fnparams in self._map.items():
            if issubclass(type, cls):
                fn = self._map[type]
                args = self._get_method_args(fn[0], fn[1])
                rv = self._cache[cls] = fn[0](self, *args)
                return rv

        if inspect.isbuiltin(cls):
            raise ValueError('No known method to construct %s' % cls)

        fn = cls.__init__
        spec = inspect.getfullargspec(fn)
        args = self._get_method_args(fn, spec)

        return cls(*args)

    def get(self, cls):
        if inspect.isclass(cls):
            return self._get_class(cls)
        else:
            raise TypeError('non-class object requested from container')

class Test1:
    def __init__(self, a):
        self.a = a

class Test2:
    def __init__(self, x:Test1, b=0, c=-1):
        self.a = x.a + b + c

class Test3:
    def __init__(self, x:Test1, y:Test2, c=400, d=-50):
        self.a = x.a + y.a + c + d

class MyContainer(Container):
    @Container.definition()
    def new1(self) -> Test1:
        return Test1(100)

print(MyContainer().get(Test3).a)

# class MyContainer(Container):
#     @Container.definition()
#     def new_int(self) -> int:
#         return 104

#     @Container.definition()
#     def new_str(self) -> str:
#         return "hello"

#     @Container.definition()
#     def new_tuple(self, a:int, b:str) -> tuple:
#         return a * 2, b

# class Testing:
#     pass

# class Testing2(Testing):
#     pass

# class Testing3(Testing):
#     def __init__(self, a:int, b:tuple):
#         self.a = a
#         self.b = b

# class Testing4:
#     def __init__(self, x:Testing3):
#         self.x = x

# x = MyContainer()
# t2 = x.get(Testing2)
# t = x.get(Testing)
# t4 = x.get(Testing4)

# print(t2)
# print(t)
# print(t4.x.a, t4.x.b)

Embed on website

To embed this program on your website, copy the following code and paste it into your website's HTML: