Coverage for src/lexigram/notification/mailer/module.py: 100%

19 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 02:32 +0800

1"""MailerModule — IoC module for email delivery.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Any 

6 

7from lexigram.contracts.mailer.protocols import MailerProtocol 

8from lexigram.di.module import DynamicModule, Module, module 

9 

10if TYPE_CHECKING: 

11 from lexigram.notification.config import MailerConfig 

12 

13 

14@module(is_global=True) 

15class MailerModule(Module): 

16 """Email delivery integration with Named DI multi-backend support. 

17 

18 Registers :class:`~lexigram.contracts.mailer.protocols.MailerProtocol` for 

19 constructor injection, supporting SMTP and SendGrid backends. 

20 

21 Usage:: 

22 

23 from lexigram.notification.config import MailerConfig, NamedMailerConfig 

24 from lexigram.notification.mailer.module import MailerModule 

25 

26 @module( 

27 imports=[ 

28 MailerModule.configure( 

29 MailerConfig( 

30 backends=[ 

31 NamedMailerConfig( 

32 name="transactional", 

33 primary=True, 

34 driver="sendgrid", 

35 ) 

36 ] 

37 ) 

38 ) 

39 ] 

40 ) 

41 class AppModule(Module): 

42 pass 

43 

44 Named injection:: 

45 

46 class MyService: 

47 def __init__( 

48 self, 

49 mailer: MailerProtocol, # primary 

50 bulk: Annotated[MailerProtocol, Named("bulk")], # named 

51 ) -> None: ... 

52 """ 

53 

54 @classmethod 

55 def configure(cls, config: MailerConfig | Any | None = None) -> DynamicModule: 

56 """Create a MailerModule with explicit configuration. 

57 

58 Args: 

59 config: :class:`~lexigram.notification.config.MailerConfig` or ``None`` 

60 to use defaults (no backends). 

61 

62 Returns: 

63 A :class:`~lexigram.di.module.DynamicModule` descriptor. 

64 """ 

65 from lexigram.notification.di.mailer_provider import MailerProvider 

66 

67 return DynamicModule( 

68 module=cls, 

69 providers=[MailerProvider(config=config)], 

70 exports=[MailerProtocol], 

71 ) 

72 

73 @classmethod 

74 def stub(cls, config: MailerConfig | Any | None = None) -> DynamicModule: 

75 """Return a MailerModule for unit tests — no emails sent. 

76 

77 Uses an empty config (no backends configured) so all mail operations 

78 are dropped and no external services are contacted. 

79 

80 Args: 

81 config: Optional :class:`~lexigram.notification.config.MailerConfig` 

82 for test scenarios that need specific stubs. 

83 

84 Returns: 

85 A :class:`~lexigram.di.module.DynamicModule` descriptor. 

86 """ 

87 from lexigram.notification.config import MailerConfig as _MailerConfig 

88 from lexigram.notification.di.mailer_provider import MailerProvider 

89 

90 stub_config = config or _MailerConfig() 

91 if not stub_config.backends: 

92 stub_config.console_fallback = False 

93 return DynamicModule( 

94 module=cls, 

95 providers=[MailerProvider(config=stub_config)], 

96 exports=[MailerProtocol], 

97 ) 

98 

99 

100__all__ = ["MailerModule"]