Skip to content

Instantly share code, notes, and snippets.

@seppeljordan
Created September 1, 2021 12:38
Show Gist options
  • Save seppeljordan/8051320d997be506ed585270c44e101b to your computer and use it in GitHub Desktop.
Save seppeljordan/8051320d997be506ed585270c44e101b to your computer and use it in GitHub Desktop.
from __future__ import annotations
from abc import ABC, abstractmethod
from functools import lru_cache
from typing import List
cache = lru_cache()
class WorkDoer:
def __init__(self, mail_sender: MailSender):
self.mail_sender = mail_sender
def do_work(self) -> None:
print("work done")
self.mail_sender.send_mail("work done")
class Logger:
def __init__(self, level: int):
print("instantiated logger")
self.level = level
def log(self, message: str) -> None:
if self.level > 1:
print(f"log: {message}")
def get_logger_config() -> int:
return 2
class MailSender(ABC):
@abstractmethod
def send_mail(self, message: str) -> None:
pass
class TestMailSender(MailSender):
def __init__(self) -> None:
self.sent_mails: List[str] = []
def send_mail(self, message: str) -> None:
self.sent_mails.append(message)
def get_sent_mails(self) -> List[str]:
return self.sent_mails
class DependencyInjector(ABC):
# since the following function call is cached, the logger is
# functionally a singleton.
@cache
def get_logger(self) -> Logger:
return Logger(self.get_logger_configuration())
def get_logger_configuration(self) -> int:
return get_logger_config()
@abstractmethod
def get_mail_sender(self) -> MailSender:
pass
def get_work_doer(self) -> WorkDoer:
return WorkDoer(self.get_mail_sender())
class TestDependencyInjector(DependencyInjector):
@cache
def get_test_mail_sender(self) -> TestMailSender:
return TestMailSender()
def get_mail_sender(self) -> MailSender:
return self.get_test_mail_sender()
# in your tests:
injector = TestDependencyInjector()
work_doer = injector.get_work_doer()
work_doer.do_work()
# The following won't actually work but is instead intended as an
# illustration as one would go about using DI in django.
class DjangoDependencyInjector(DependencyInjector):
def get_mail_sender(self) -> MailSender:
return TestMailSender()
def injection_view(view):
def wrapped_view(request, **kwargs):
injector = DjangoDependencyInjector()
return view(request, injector, **kwargs)
return wrapped_view
@injection_view
def do_work_view(request, dep_injector: DependencyInjector):
work_doer = dep_injector.get_work_doer()
work_doer.do_work()
@4lm
Copy link

4lm commented Sep 4, 2021

Hi @seppeljordan, I followed up on our lecture and played with your code to get the hang of it. Next stop: injector

If you like, you can give me comments on my changes, here in this gist or next week in our meeting - I mainly:

  • added a logs list
  • added a pythonic way for the properties logs and sent_mails [1] [2]
  • printed logged logs list and sent mails list
  • renamed the property @cache to @singleton because IMO this better reflects its purpose

Fun fact, I was thinking to myself "yeah, and after you made all those changes you have to adjust the Django example because it will most definately be broken somehow". I did not have to change a single thing 🥳

Code:

from __future__ import annotations

from abc import ABC, abstractmethod
from functools import lru_cache
from typing import List

singleton = lru_cache(maxsize=1)


def logger_config() -> int:
    return 2


class WorkDoer:
    def __init__(self, mail_sender: MailSender, logger: Logger):
        self.mail_sender = mail_sender
        self.logger = logger

    def do_work(self) -> None:
        message = "Work done"
        self.mail_sender.send_mail(message)
        self.logger.log(message)


class Logger:
    def __init__(self, level: int):
        self._logs: List[str] = []
        # Make sure to log init message
        self.level = 2
        self.log("Logger instantiated")
        # Set to exernally defined level
        self.level = level

    def log(self, message: str) -> None:
        if self.level > 1:
            print(f"log: {message}")
            self._logs.append(message)

    @property
    def logs(self) -> List[str]:
        return self._logs


class MailSender(ABC):
    @abstractmethod
    def send_mail(self, message: str) -> None:
        pass

    @abstractmethod
    def sent_mails(self) -> None:
        pass


class TestMailSender(MailSender):
    def __init__(self):
        self._sent_mails: List[str] = []

    def send_mail(self, message: str) -> None:
        print(f"send_mail: {message}")
        self._sent_mails.append(message)

    @property
    def sent_mails(self) -> List[str]:
        return self._sent_mails


class DependencyInjector(ABC):
    @singleton
    def get_logger(self) -> Logger:
        return Logger(self.get_logger_configuration())

    def get_logger_configuration(self) -> int:
        return logger_config()

    @abstractmethod
    def get_mail_sender(self) -> MailSender:
        pass

    def get_work_doer(self) -> WorkDoer:
        return WorkDoer(self.get_mail_sender(), self.get_logger())


class TestDependencyInjector(DependencyInjector):
    @singleton
    def get_test_mail_sender(self) -> TestMailSender:
        return TestMailSender()

    def get_mail_sender(self) -> MailSender:
        return self.get_test_mail_sender()


# in your tests:

injector = TestDependencyInjector()

work_doer = injector.get_work_doer()
logger = injector.get_logger()
mail_sender = injector.get_mail_sender()

work_doer.do_work()

print(f"sent_mails: {mail_sender.sent_mails}")
print(f"logs: {logger.logs}")


# The following won't actually work but is instead intended as an
# illustration as one would go about using DI in django.


class DjangoDependencyInjector(DependencyInjector):
    def get_mail_sender(self) -> MailSender:
        return TestMailSender()


def injection_view(view):
    def wrapped_view(request, **kwargs):
        injector = DjangoDependencyInjector()
        return view(request, injector, **kwargs)

    return wrapped_view


@injection_view
def do_work_view(request, dep_injector: DependencyInjector):
    work_doer = dep_injector.get_work_doer()
    work_doer.do_work()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment