diff --git a/anydi/_decorators.py b/anydi/_decorators.py index 28f7d63..0806c66 100644 --- a/anydi/_decorators.py +++ b/anydi/_decorators.py @@ -161,7 +161,9 @@ class Provided(Protocol): def is_provided(cls: Any) -> TypeGuard[type[Provided]]: - return hasattr(cls, "__provided__") and "scope" in cls.__provided__ + # A subclass inherits `__provided__`, but nobody marked it. + metadata = getattr(cls, "__dict__", {}).get("__provided__") + return metadata is not None and "scope" in metadata class ProviderMetadata(TypedDict): diff --git a/anydi/_module.py b/anydi/_module.py index 86a4344..fd24566 100644 --- a/anydi/_module.py +++ b/anydi/_module.py @@ -15,11 +15,16 @@ class ModuleMeta(type): """A metaclass used for the Module base class.""" def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> Any: - attrs["providers"] = [ + # Keyed by name, so a subclass replaces a provider it redefines. + providers: dict[str, ProviderMetadata] = {} + for base in reversed(bases): + providers.update(getattr(base, "providers", ())) + providers.update( (name, value.__provider__) for name, value in attrs.items() if is_provider(value) - ] + ) + attrs["providers"] = list(providers.items()) return super().__new__(cls, name, bases, attrs) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 1953ea6..1678cd5 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -347,3 +347,27 @@ def my_func() -> None: assert my_func.__injectable__ == { "tags": ["tag1", "tag2"], } + + +def test_is_not_provided_by_inheritance() -> None: + @singleton + class BaseSender: + pass + + class SmtpSender(BaseSender): + pass + + assert is_provided(BaseSender) + assert not is_provided(SmtpSender) + + +def test_a_decorated_subclass_is_provided() -> None: + @singleton + class BaseSender: + pass + + @singleton + class SmtpSender(BaseSender): + pass + + assert is_provided(SmtpSender) diff --git a/tests/test_module.py b/tests/test_module.py index 96cea5d..368f14b 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -166,3 +166,47 @@ def cache(self) -> RedisCache: cache2 = container.resolve(IStore) cache3 = container.resolve(RedisCache) assert cache1 is cache2 is cache3 + + def test_module_subclass_keeps_the_base_providers( + self, container: Container + ) -> None: + class Config: + pass + + class Repo: + def __init__(self, config: Config) -> None: + self.config = config + + class BaseModule(Module): + @provider(scope="singleton") + def config(self) -> Config: + return Config() + + class AppModule(BaseModule): + @provider(scope="singleton") + def repo(self, config: Config) -> Repo: + return Repo(config=config) + + container.register_module(AppModule) + + assert container.is_registered(Config) + assert isinstance(container.resolve(Repo).config, Config) + + def test_module_subclass_replaces_a_provider_it_redefines( + self, container: Container + ) -> None: + class BaseModule(Module): + @provider(scope="singleton") + def message(self) -> str: + return "base" + + class AppModule(BaseModule): + @provider(scope="singleton") + def message(self) -> str: + return "app" + + assert [name for name, _ in AppModule.providers] == ["message"] + + container.register_module(AppModule) + + assert container.resolve(str) == "app"