Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion anydi/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
9 changes: 7 additions & 2 deletions anydi/_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
24 changes: 24 additions & 0 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
44 changes: 44 additions & 0 deletions tests/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading