Repository: reagento/dishka
Pull request #118 (2024-03-29 11:02:15)

0.9.0
Pre 1.0 release


Patch:
diff --git a/README.md b/README.md
index 3e74a15..6c381d9 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Cute DI framework with scopes and agreeable API.
 
 ### Purpose
 
-This library is targeting to provide only an IoC-container but make it really useful. If you are tired manually passing objects to create others objects which are only used to create more object - we have a solution. Not all project require an IoC-container, but check what we have.
+This library is targeting to provide only an IoC-container but make it really useful. If you are tired manually passing objects to create others objects which are only used to create more objects - we have a solution. Not all project require an IoC-container, but check what we have.
 
 Unlike other instruments we are not trying to solve tasks not related to dependency injection. We want to keep DI in place, not soiling you code with global variables and additional specifiers in all places.
 
diff --git a/docs/advanced/components.rst b/docs/advanced/components.rst
index 4d6cab6..612b7b1 100644
--- a/docs/advanced/components.rst
+++ b/docs/advanced/components.rst
@@ -1,3 +1,5 @@
+.. _components:
+
 Components and providers isolation
 ****************************************
 
diff --git a/docs/advanced/context.rst b/docs/advanced/context.rst
new file mode 100644
index 0000000..2613480
--- /dev/null
+++ b/docs/advanced/context.rst
@@ -0,0 +1,41 @@
+Context data
+====================
+
+Often, your scopes are assigned with some external events: HTTP-requests, message from queue, callbacks from framework. You can use those objects when creating dependencies.
+
+The difference from normal factories is that they are not created inside some ``Provider``, but passed to the scope.
+
+Working with context data consists of three parts:
+
+1. Declaration that object is received from context using :ref:`from-context`. You need to provide the type and scope.
+2. Usage of that object in providers. There is now difference how the object is
+3. Passing actual values on scope entrance. It can be container creation for top level scope or container calls for nested ones. Use it in form ``context={Type: value,...}``
+
+.. code-block:: python
+
+    from framework import Request
+
+    class MyProvider:
+        scope=Scope.REQUEST
+
+        # declare source
+        request = from_context(provides=Request, scope=Scope.REQUEST)
+        event_broker = from_context(provides=Broker, scope=Scope.APP)
+
+        # use objects as usual
+        @provide
+        def a(self, request: Request, broker: Broker) -> A:
+            return A(data=request.contents)
+
+    # provide APP-scoped context variable
+    container = make_container(MyProvider(), context={Broker: broker})
+
+    while True:
+        request = broker.recv()
+        # provide REQUEST-scoped context variable
+        with container(context={Request:request}) as request_container:
+             a = request_container.get(A)
+
+.. note::
+
+    If your are using *multiple components*, you need to specify ``from_context`` in them separately though the context is shared. Context data is always stored in default component, so, other components may not use it and have factories instead.
\ No newline at end of file
diff --git a/docs/advanced/scopes.rst b/docs/advanced/scopes.rst
index 9cc23b6..ac15499 100644
--- a/docs/advanced/scopes.rst
+++ b/docs/advanced/scopes.rst
@@ -1,4 +1,5 @@
 .. include:: <isonum.txt>
+.. _scopes:
 
 Scope management
 *************************
diff --git a/docs/concepts.rst b/docs/concepts.rst
index 70327c1..a281d44 100644
--- a/docs/concepts.rst
+++ b/docs/concepts.rst
@@ -47,6 +47,8 @@ If you are developing web application, you would enter ``APP`` scope on startup,
 
 You can provide your own Scopes class if you are not satisfied with standard flow.
 
+:ref:`Read more about custom and hidden scopes<scopes>`
+
 Container
 ==================
 
@@ -71,6 +73,9 @@ Async container is working in the same manner, but you should use async context
 
 Some containers are concurrently safe, others are not: it is configured when you call a context manager. For web applications it is good to have APP-scoped container thread/task-safe, but REQUEST-scoped containers do not it, and it is default behavior.
 
+
+:ref:`Read more about container API<container>`
+
 Provider
 ===============
 
@@ -119,4 +124,6 @@ This allows you to have multiple parts of application build separately without n
 
     container = make_container(MainProvider(), AdditionalProvider())
     container.get(float)  # returns 0.1
-    container.get(complex)  # raises NoFactoryError
\ No newline at end of file
+    container.get(complex)  # raises NoFactoryError
+
+:ref:`Read more about components management<components>`
\ No newline at end of file
diff --git a/docs/container/index.rst b/docs/container/index.rst
index 5b9d24c..b192635 100644
--- a/docs/container/index.rst
+++ b/docs/container/index.rst
@@ -1,3 +1,5 @@
+.. _container:
+
 Container
 *******************
 
@@ -103,26 +105,3 @@ To prevent such a condition you need to protect any session whose children can b
 
 .. note::
     Do not worry, lock is set by default for top level (``Scope.APP``) container. So, if you are not using other scopes concurrently you do not need any changes. (E.g. if you are not using multiple ``Scope.ACTION`` containers at a same time within one ``Scope.REQUEST`` container)
-
-Context data
-====================
-
-Often, your scopes are assigned with some external events: HTTP-requests, message from queue, callbacks from framework. You can use those objects when creating dependencies. The difference from normal factories is that they are not created inside some ``Provider``, but passed to the scope. You need explicitly tell dishka which dependencies are expected to be received from context using ``from_context``
-
-.. code-block:: python
-
-    from framework import Request
-
-    class MyProvider:
-        request = from_context(provides=Request, scope=Scope.REQUEST)
-
-        @provide(scope=Scope.REQUEST)
-        def a(self, request: Request) -> A:
-            return A(data=request.contents)
-
-    container = make_container(MyProvider())
-
-    while True:
-        request = connection.recv()
-        with container(context={Request:request}) as request_container:
-             a = request_container.get(A)
diff --git a/docs/index.rst b/docs/index.rst
index 86c0683..143aac1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -4,21 +4,46 @@ dishka
 Cute DI framework with scopes and agreeable API.
 
 This library is targeting to provide only an IoC-container but make it really useful.
+If you are tired manually passing objects to create others objects which are only used to create more objects - we have a solution.
+
+Unlike other instruments we are not trying to solve tasks not related to dependency injection. We want to keep DI in place, not soiling you code with global variables and additional specifiers in all places.
+
+Main ideas:
+
+* **Scopes**. Any object can have lifespan of the whole app, single request or even more fractionally. Many frameworks do not have scopes or have only 2 of them. Here you can have as many scopes as you need.
+* **Finalization**. Some dependencies like database connections must be not only created, but carefully released. Many framework lack this essential feature
+* **Modular providers**. Instead of creating lots of separate functions or contrariwise a big single class, you can split your factories into several classes, which makes them simpler reusable.
+* **Clean dependencies**. You do not need to add custom markers to the code of dependencies so to allow library to see them. All customization is done within providers code and only borders of scopes have to deal with library API.
+* **Simple API**. You need minimum of objects to start using library. You can easily integrate it with your task framework, examples provided.
+* **Speed**. It is fast enough so you not to worry about. It is even faster than many of the analogs.
+
 
 .. toctree::
+   :hidden:
    :caption: Contents:
 
    quickstart
    di_intro
    concepts
-   requirements/technical
    provider/index
    container/index
    integrations/index
+   alternatives
+
+.. toctree::
+   :hidden:
+   :caption: Advanced usage:
+
    advanced/components
+   advanced/context
    advanced/scopes
    advanced/testing/index
-   alternatives
+
+.. toctree::
+   :hidden:
+   :caption: For developers:
+
+   requirements/technical
    contributing
 
 .. toctree::
diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst
index 5e183e1..b9af24c 100644
--- a/docs/integrations/index.rst
+++ b/docs/integrations/index.rst
@@ -106,6 +106,27 @@ With some frameworks we provide an option to inject dependencies in handlers wit
     setup_dishka(container, app)
 
 
+Context data
+====================
+
+As ``REQUEST`` scope is entered automatically you cannot pass context data directly, but integrations do it for you:
+
+This objects are passed to context:
+
+* aiohttp - ``aiohttp.web_request.Request``
+* Flask - ``flask.Request``
+* Fastapi - ``fastapi.Request``
+* Litestar - ``litestar.Request``
+* Starlette - ``starlette.requests.Request``
+* Aiogram - ``aiogram.types.TelegramObject``
+* pyTelegramBotAPI - actual type of event (like ``Message``) is used.
+* Arq - no objects
+* FastStream - no objects
+* TaskIq - no objects
+
+To use such objects you need to declare them in your provider using :ref:`from-context` and then they will be available as factories params.
+
+
 Adding integrations
 ===========================
 
diff --git a/docs/requirements/technical.rst b/docs/requirements/technical.rst
index 07f2f9a..6616eb4 100644
--- a/docs/requirements/technical.rst
+++ b/docs/requirements/technical.rst
@@ -36,6 +36,8 @@ Technical requirements
 
 1. Dependencies which require some cleanup must be cleaned up on the scope exit
 2. Dependencies which do not require cleanup should somehow be supported
+3. Dependencies are cleaned in reverse order as they created
+4. Exceptions should not prevent cleaning of then rest of dependencies
 
 5. Context data
 ================
@@ -59,6 +61,9 @@ Technical requirements
 2. When creating a dependency there should be a way to decide which subtype is used and request only its dependencies
 3. There should be a way to reuse same object for multiple requested types
 4. There should be a way to decorate dependency just adding new providers
+5. There should be a way to enter multiple scopes with single call. The last of those scopes is used when dependencies are requested.
+6. Container user errors must be clear and contain information necessary for resolution
+7. Dependency graph should be prematurely analyzed to detect most obvious errors
 
 8. Integration
 ================
diff --git a/pyproject.toml b/pyproject.toml
index 3cdc91c..9a06ec2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ where = ["src"]
 
 [project]
 name = "dishka"
-version = "0.8.0"
+version = "0.1"
 readme = "README.md"
 authors = [
     { name = "Andrey Tikhonov", email = "17@itishka.org" },
diff --git a/src/dishka/dependency_source/make_decorator.py b/src/dishka/dependency_source/make_decorator.py
index 3478e8f..ac0b613 100644
--- a/src/dishka/dependency_source/make_decorator.py
+++ b/src/dishka/dependency_source/make_decorator.py
@@ -10,10 +10,13 @@ from .unpack_provides import unpack_decorator
 def _decorate(
         source: Callable | type | None = None,
         provides: Any = None,
+        *,
+        is_in_class: bool = True,
 ) -> CompositeDependencySource:
     composite = ensure_composite(source)
     decorator = Decorator(make_factory(
         provides=provides, scope=None, source=source, cache=False,
+        is_in_class=is_in_class,
     ))
     composite.dependency_sources.extend(unpack_decorator(decorator))
     return composite
@@ -39,12 +42,19 @@ def decorate(
 def decorate(
         source: Callable | type | None = None,
         provides: Any = None,
-) -> CompositeDependencySource | Callable[
-    [Callable], CompositeDependencySource]:
+) -> Any:
     if source is not None:
-        return _decorate(source, provides)
+        return _decorate(source, provides, is_in_class=True)
 
     def scoped(func):
-        return _decorate(func, provides)
+        return _decorate(func, provides, is_in_class=True)
 
     return scoped
+
+
+def decorate_on_instance(
+        source: Callable | type | None = None,
+        provides: Any = None,
+) -> CompositeDependencySource:
+    return _decorate(source, provides, is_in_class=False)
+
diff --git a/src/dishka/dependency_source/make_factory.py b/src/dishka/dependency_source/make_factory.py
index b8858bf..6c9c9fd 100644
--- a/src/dishka/dependency_source/make_factory.py
+++ b/src/dishka/dependency_source/make_factory.py
@@ -6,6 +6,7 @@ from collections.abc import (
     Generator,
     Iterable,
     Iterator,
+    Sequence,
 )
 from inspect import (
     isasyncgenfunction,
@@ -15,6 +16,7 @@ from inspect import (
     isgeneratorfunction,
     ismethod,
     signature,
+    unwrap,
 )
 from typing import (
     Any,
@@ -28,6 +30,7 @@ from dishka._adaptix.type_tools.basic_utils import (
     get_all_type_hints,
     get_type_vars,
     is_bare_generic,
+    strip_alias,
 )
 from dishka._adaptix.type_tools.generic_resolver import (
     GenericResolver,
@@ -39,6 +42,8 @@ from .composite import CompositeDependencySource, ensure_composite
 from .factory import Factory, FactoryType
 from .unpack_provides import unpack_factory
 
+_empty = signature(lambda a: 0).parameters["a"].annotation
+
 
 def _is_bound_method(obj):
     return ismethod(obj) and obj.__self__
@@ -69,34 +74,76 @@ def _guess_factory_type(source):
         return FactoryType.FACTORY
 
 
-def _async_generator_result(possible_dependency: Any):
-    origin = get_origin(possible_dependency)
+def _type_repr(hint: Any) -> str:
+    if hint is type(None):
+        return "None"
+    module = getattr(hint, "__module__", "")
+    if module == "builtins":
+        module = ""
+    elif module:
+        module += "."
+    try:
+        return f"{module}{hint.__qualname__}"
+    except AttributeError:
+        return str(hint)
+
+
+def _async_generator_result(hint: Any):
+    origin = get_origin(hint)
     if origin is AsyncIterable:
-        return get_args(possible_dependency)[0]
+        return get_args(hint)[0]
     elif origin is AsyncIterator:
-        return get_args(possible_dependency)[0]
+        return get_args(hint)[0]
     elif origin is AsyncGenerator:
-        return get_args(possible_dependency)[0]
+        return get_args(hint)[0]
+    # errors
+    name = _type_repr(hint)
+    if origin is Iterable:
+        args = ", ".join(_type_repr(a) for a in get_args(hint))
+        guess = "AsyncIterable"
+    elif origin is Iterator:
+        args = ", ".join(_type_repr(a) for a in get_args(hint))
+        guess = "AsyncIterator"
+    elif origin is Generator:
+        args = ", ".join(_type_repr(a) for a in get_args(hint)[:2])
+        guess = "AsyncGenerator"
     else:
-        raise TypeError(
-            f"Unsupported return type {possible_dependency} {origin} "
-            f"for async generator",
-        )
+        args = name
+        guess = "AsyncIterable"
+
+    raise TypeError(
+        f"Unsupported return type `{name}` for async generator. "
+        f"Did you mean {guess}[{args}]?",
+    )
 
 
-def _generator_result(possible_dependency: Any):
-    origin = get_origin(possible_dependency)
+def _generator_result(hint: Any):
+    origin = get_origin(hint)
     if origin is Iterable:
-        return get_args(possible_dependency)[0]
+        return get_args(hint)[0]
     elif origin is Iterator:
-        return get_args(possible_dependency)[0]
+        return get_args(hint)[0]
     elif origin is Generator:
-        return get_args(possible_dependency)[1]
+        return get_args(hint)[1]
+    # errors
+    name = _type_repr(hint)
+    if origin is AsyncIterable:
+        args = ", ".join(_type_repr(a) for a in get_args(hint))
+        guess = "Iterable"
+    elif origin is AsyncIterator:
+        args = ", ".join(_type_repr(a) for a in get_args(hint))
+        guess = "Iterator"
+    elif origin is AsyncGenerator:
+        args = ", ".join(_type_repr(a) for a in get_args(hint)) + ", None"
+        guess = "Generator"
     else:
-        raise TypeError(
-            f"Unsupported return type {possible_dependency} {origin}"
-            f" for generator",
-        )
+        args = name
+        guess = "Iterable"
+
+    raise TypeError(
+        f"Unsupported return type `{name}` for generator. "
+        f"Did you mean {guess}[{args}]?",
+    )
 
 
 def _clean_result_hint(factory_type: FactoryType, possible_dependency: Any):
@@ -107,6 +154,18 @@ def _clean_result_hint(factory_type: FactoryType, possible_dependency: Any):
     return possible_dependency
 
 
+def _params_without_hints(func, *, skip_self: bool) -> Sequence[str]:
+    if func is object.__init__:
+        return []
+    params = signature(func).parameters
+    return [
+        p.name
+        for i, p in enumerate(params.values())
+        if p.annotation is _empty
+        if i > 0 or not skip_self
+    ]
+
+
 def _make_factory_by_class(
         *,
         provides: Any,
@@ -114,11 +173,30 @@ def _make_factory_by_class(
         source: Callable,
         cache: bool,
 ) -> Factory:
+    init = strip_alias(source).__init__
+    if missing_hints := _params_without_hints(init, skip_self=True):
+        name = f"{source.__module__}.{source.__qualname__}.__init__"
+        missing = ", ".join(missing_hints)
+        raise ValueError(
+            f"Failed to analyze `{name}`. \n"
+            f"Some parameters do not have type hints: {missing}\n",
+        )
     # we need to fix concrete generics and normal classes as well
     # as classes can be children of concrete generics
     res = GenericResolver(_get_init_members)
-    hints = dict(res.get_resolved_members(source).members)
-    hints.pop("return", None)
+    try:
+        hints = dict(res.get_resolved_members(source).members)
+    except NameError as e:
+        name = f"{source.__module__}.{source.__qualname__}.__init__"
+        raise NameError(
+            f"Failed to analyze `{name}`. \n"
+            f"Type '{e.name}' is not defined\n\n"
+            f"If your are using `if TYPE_CHECKING` to import '{e.name}' "
+            f"then try removing it. \n"
+            f"Or, create a separate factory with all types imported.",
+            name=e.name,
+        ) from e
+    hints.pop("return", _empty)
     dependencies = list(hints.values())
     if not provides:
         provides = source
@@ -141,35 +219,58 @@ def _make_factory_by_method(
         scope: BaseScope | None,
         source: Callable | classmethod,
         cache: bool,
+        is_in_class: bool,
 ) -> Factory:
-    if isinstance(source, classmethod):
-        params = signature(source.__wrapped__).parameters
-        factory_type = _guess_factory_type(source.__wrapped__)
-    else:
-        params = signature(source).parameters
-        factory_type = _guess_factory_type(source)
+    raw_source = unwrap(source)
+    missing_hints = _params_without_hints(raw_source, skip_self=is_in_class)
+    if missing_hints:
+        name = getattr(source, "__qualname__", "") or str(source)
+        missing = ", ".join(missing_hints)
+        raise ValueError(
+            f"Failed to analyze `{name}`. \n"
+            f"Some parameters do not have type hints: {missing}\n",
+        )
 
-    hints = get_type_hints(source, include_extras=True)
-    self = next(iter(params.values()), None)
-    if self:
-        if self.name not in hints:
+    params = signature(raw_source).parameters
+    factory_type = _guess_factory_type(raw_source)
+
+    try:
+        hints = get_type_hints(source, include_extras=True)
+    except NameError as e:
+        name = getattr(source, "__qualname__", "") or str(source)
+        raise NameError(
+            f"Failed to analyze `{name}`. \n"
+            f"Type '{e.name}' is not defined. \n\n"
+            f"If your are using `if TYPE_CHECKING` to import '{e.name}' "
+            f"then try removing it. \n"
+            f"Or, create a separate factory with all types imported.",
+            name=e.name,
+        ) from e
+    if is_in_class:
+        self = next(iter(params.values()), None)
+        if self and self.name not in hints:
             # add self to dependencies, so it can be easily removed
             # if we will bind factory to provider instance
             hints = {self.name: Any, **hints}
-        is_to_bind = True
-    else:
-        is_to_bind = False
-    possible_dependency = hints.pop("return", None)
+    possible_dependency = hints.pop("return", _empty)
     dependencies = list(hints.values())
     if not provides:
-        provides = _clean_result_hint(factory_type, possible_dependency)
+        if possible_dependency is _empty:
+            name = getattr(source, "__qualname__", "") or str(source)
+            raise ValueError(f"Failed to analyze `{name}`. \n"
+                             f"Missing return type hint.")
+        try:
+            provides = _clean_result_hint(factory_type, possible_dependency)
+        except TypeError as e:
+            name = getattr(source, "__qualname__", "") or str(source)
+            raise TypeError(f"Failed to analyze `{name}`. \n" + str(e)) from e
     return Factory(
         dependencies=hints_to_dependency_keys(dependencies),
         type_=factory_type,
         source=source,
         scope=scope,
         provides=DependencyKey(provides, None),
-        is_to_bind=is_to_bind,
+        is_to_bind=is_in_class,
         cache=cache,
     )
 
@@ -181,12 +282,38 @@ def _make_factory_by_static_method(
         source: staticmethod,
         cache: bool,
 ) -> Factory:
+    if missing_hints := _params_without_hints(source, skip_self=False):
+        name = getattr(source, "__qualname__", "") or str(source)
+        missing = ", ".join(missing_hints)
+        raise ValueError(
+            f"Failed to analyze `{name}`. \n"
+            f"Some parameters do not have type hints: {missing}\n",
+        )
     factory_type = _guess_factory_type(source.__wrapped__)
-    hints = get_type_hints(source, include_extras=True)
-    possible_dependency = hints.pop("return", None)
+    try:
+        hints = get_type_hints(source, include_extras=True)
+    except NameError as e:
+        name = getattr(source, "__qualname__", "") or str(source)
+        raise NameError(
+            f"Failed to analyze `{name}`. \n"
+            f"Type '{e.name}' is not defined. \n\n"
+            f"If your are using `if TYPE_CHECKING` to import '{e.name}' "
+            f"then try removing it. \n"
+            f"Or, create a separate factory with all types imported.",
+            name=e.name,
+        ) from e
+    possible_dependency = hints.pop("return", _empty)
     dependencies = list(hints.values())
     if not provides:
-        provides = _clean_result_hint(factory_type, possible_dependency)
+        if possible_dependency is _empty:
+            name = getattr(source, "__qualname__", "") or str(source)
+            raise ValueError(f"Failed to analyze `{name}`. \n"
+                             f"Missing return type hint.")
+        try:
+            provides = _clean_result_hint(factory_type, possible_dependency)
+        except TypeError as e:
+            name = getattr(source, "__qualname__", "") or str(source)
+            raise TypeError(f"Failed to analyze `{name}`. \n" + str(e)) from e
     return Factory(
         dependencies=hints_to_dependency_keys(dependencies),
         type_=factory_type,
@@ -214,6 +341,7 @@ def _make_factory_by_other_callable(
         source=to_check,
         cache=cache,
         scope=scope,
+        is_in_class=True,
     )
     if factory.is_to_bind:
         dependencies = factory.dependencies[1:]  # remove `self`
@@ -236,6 +364,7 @@ def make_factory(
         scope: BaseScope | None,
         source: Callable,
         cache: bool,
+        is_in_class: bool,
 ) -> Factory:
     if is_bare_generic(source):
         source = source[get_type_vars(source)]
@@ -247,6 +376,7 @@ def make_factory(
     elif isfunction(source) or isinstance(source, classmethod):
         return _make_factory_by_method(
             provides=provides, scope=scope, source=source, cache=cache,
+            is_in_class=is_in_class,
         )
     elif isinstance(source, staticmethod):
         return _make_factory_by_static_method(
@@ -266,15 +396,31 @@ def _provide(
         scope: BaseScope | None = None,
         provides: Any = None,
         cache: bool = True,
+        is_in_class: bool = True,
 ) -> CompositeDependencySource:
     composite = ensure_composite(source)
     factory = make_factory(
-        provides=provides, scope=scope, source=composite.origin, cache=cache,
+        provides=provides, scope=scope,
+        source=composite.origin, cache=cache,
+        is_in_class=is_in_class,
     )
     composite.dependency_sources.extend(unpack_factory(factory))
     return composite
 
 
+def provide_on_instance(
+        *,
+        source: Callable | classmethod | staticmethod | type | None = None,
+        scope: BaseScope | None = None,
+        provides: Any = None,
+        cache: bool = True,
+) -> CompositeDependencySource:
+    return _provide(
+        provides=provides, scope=scope, source=source, cache=cache,
+        is_in_class=False,
+    )
+
+
 @overload
 def provide(
         *,
@@ -326,11 +472,13 @@ def provide(
     if source is not None:
         return _provide(
             provides=provides, scope=scope, source=source, cache=cache,
+            is_in_class=True,
         )
 
     def scoped(func):
         return _provide(
             provides=provides, scope=scope, source=func, cache=cache,
+            is_in_class=True,
         )
 
     return scoped
diff --git a/src/dishka/provider.py b/src/dishka/provider.py
index 5c1f4c5..79355ef 100644
--- a/src/dishka/provider.py
+++ b/src/dishka/provider.py
@@ -11,11 +11,11 @@ from .dependency_source import (
     DependencySource,
     Factory,
     alias,
-    decorate,
     from_context,
-    provide,
 )
 from .dependency_source.composite import CompositeDependencySource
+from .dependency_source.make_decorator import decorate_on_instance
+from .dependency_source.make_factory import provide_on_instance
 
 
 def is_dependency_source(attribute: Any) -> bool:
@@ -63,6 +63,38 @@ class Provider(BaseProvider):
         for name, composite in sources:
             self._add_dependency_sources(name, composite.dependency_sources)
 
+    def _name(self):
+        if type(self) is Provider:
+            return str(self)
+        else:
+            cls = type(self)
+            return f"`{cls.__module__}.{cls.__qualname__}`"
+
+    def _source_name(self, factory: Factory) -> str:
+        source = factory.source
+        if source == factory.provides.type_hint:
+            return "`provides()`"
+        elif func := getattr(source, "__func__", None):
+            name = getattr(func, "__qualname__", None)
+            if name:
+                return f"`{name}`"
+        elif isinstance(source, type):
+            name = getattr(source, "__qualname__", None)
+            if name:
+                return f"`{source.__module__}.{name}`"
+        else:
+            name = getattr(source, "__qualname__", None)
+            if name:
+                return f"`{name}`"
+        return str(source)
+
+    def _provides_name(self, factory: Factory | ContextVariable) -> str:
+        hint = factory.provides.type_hint
+        name = getattr(hint, "__qualname__", None)
+        if name:
+            return f"`{hint.__module__}.{name}`"
+        return str(hint)
+
     def _add_dependency_sources(
             self, name: str, sources: Sequence[DependencySource],
     ) -> None:
@@ -70,10 +102,24 @@ class Provider(BaseProvider):
             if isinstance(source, Alias):
                 self.aliases.append(source)
             if isinstance(source, Factory):
+                if source.scope is None:
+                    src_name = self._source_name(source)
+                    provides_name = self._provides_name(source)
+                    raise ValueError(
+                        f"No scope is set for {provides_name}.\n"
+                        f"Set in provide() call for {src_name} or "
+                        f"within {self._name()}",
+                    )
                 self.factories.append(source)
             if isinstance(source, Decorator):
                 self.decorators.append(source)
             if isinstance(source, ContextVariable):
+                if source.scope is None:
+                    provides_name = self._provides_name(source)
+                    raise ValueError(
+                        f"No scope is set for {provides_name}.\n"
+                        f"Set in from_context() call or within {self._name()}",
+                    )
                 self.context_vars.append(source)
 
     def provide(
@@ -86,7 +132,7 @@ class Provider(BaseProvider):
     ) -> CompositeDependencySource:
         if scope is None:
             scope = self.scope
-        composite = provide(
+        composite = provide_on_instance(
             source=source,
             scope=scope,
             provides=provides,
@@ -116,7 +162,7 @@ class Provider(BaseProvider):
             *,
             provides: Any = None,
     ) -> CompositeDependencySource:
-        composite = decorate(
+        composite = decorate_on_instance(
             source=source,
             provides=provides,
         )


Test patch:
diff --git a/tests/unit/container/test_context_vars.py b/tests/unit/container/test_context_vars.py
index 1cac587..664fb12 100644
--- a/tests/unit/container/test_context_vars.py
+++ b/tests/unit/container/test_context_vars.py
@@ -57,3 +57,25 @@ async def test_2components():
 
     container = make_async_container(MyProvider(), context={int: 1})
     assert await container.get(float, component="XXX") == 1
+
+
+@pytest.mark.asyncio
+async def test_2components_factory():
+    class DefaultProvider(Provider):
+        scope = Scope.APP
+        a = from_context(provides=int)
+
+    class MyProvider(Provider):
+        scope = Scope.APP
+        component = "XXX"
+
+        @provide
+        def get_int(self) -> int:
+            return 100
+
+        @provide
+        def foo(self, a: int) -> float:
+            return a
+
+    container = make_async_container(MyProvider(), context={int: 1})
+    assert await container.get(float, component="XXX") == 100
diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py
index ac26fe3..d594392 100644
--- a/tests/unit/test_provider.py
+++ b/tests/unit/test_provider.py
@@ -89,6 +89,7 @@ def test_parse_factory_cls(source, provider_type, is_to_bound):
         source=source,
         cache=False,
         scope=Scope.REQUEST,
+        is_in_class=False,
     )
     assert factory.provides == hint_to_dependency_key(ClassA)
     assert factory.dependencies == hints_to_dependency_keys([int])
diff --git a/tests/unit/test_render.py b/tests/unit/test_render.py
index e93f304..8fb6d5e 100644
--- a/tests/unit/test_render.py
+++ b/tests/unit/test_render.py
@@ -23,6 +23,7 @@ def test_cycle(renderer):
         source=foo,
         cache=False,
         scope=None,
+        is_in_class=True,
     )
 
     res = renderer.render([factory, factory, factory])
@@ -42,6 +43,7 @@ def test_cycle_1(renderer):
         source=foo,
         cache=False,
         scope=None,
+        is_in_class=True,
     )
 
     res = renderer.render([factory])
@@ -59,6 +61,7 @@ def test_linear(renderer):
         source=foo,
         cache=False,
         scope=None,
+        is_in_class=True,
     )
 
     res = renderer.render([factory, factory], last=factory.provides)