Notes on Plugins

What are Plugins?

Plugins in the context of software, are components that extend or enhance an existing program (e.g., Slack plugins for JIRA, extends Slack functionalities to interact with JIRA content)

Plugins are also known as "extension" or "add-on"

Benefits

  • New features are easier to develop
  • Separation of concerns
  • Smaller programs
  • Third party developers can extend your app

Trade-offs (or considerations)

  • Upfront design cost
  • How will plugins interact with host application
  • Additional complexity within host application to support plugins and "worth" it

References

  • Dynamic Code Patterns: Extending Your Applications with Plugins by Doug Hellmann
  • Plug-in to Python by Rose Judge

Anatomy of a Plugin System (use as a checklist!)

  • Requires a host application
  • Communication channel between host and plugin (like function calls, over protocol like web sockets)
  • Way to register a plugin with host application (like folder specified where plugins live)
  • Load dynamically at runtime
  • Respond when called by host application

Example: Gathering Git Statistics

  • Build a CLI > submit URL > get project statistics
  • Requirements:
    • Support GitHub and GitLab upon release (future providers are plugins, like BitBucket)
    • Identify provider given URL
    • Use API to download statistics
    • Host application <- GitHub, GitLab

class RepoDetails(NamedTuple):
    organization: str
    repo: str


class RepoStatistics(NamedTuple):
    id: int
    description: str
    stars: int
    forks: int
    open_issues: int
    last_activity: datetime


class BasePlugin:
    def __init__(self, repo):
        self.repo = repo

    def __repr__(self):
        return f"<{self.__class__.__name__}>"

    @staticmethod
    def check(domain) -> bool:
        raise NotImplementedError

    def repo_stats(self) -> RepoStatistics:
        raise NotImplementedError


class GitHubPlugin(BasePlugin):

    @staticmethod
    def check(domain):
        return domain.lower() == "github.com"

    def repo_stats(self) -> RepoStatistics:
        project_url = "github/repos/{repo}/
        response = requests.get(project_url)
        data = response.json()

        return RepoStatistics(**)


class GitLabPlugin(BasePlugin):
    ...

plugins = [GitHubPlugin, GitLabPlugin]

class GitApiClient:
    def __init__(self, url):
        domain, self.repo = self._parse_url(url)
        for plugin in plugins:
            if plugin.check(domain):
                self.plugin = plugin(self.repo)
                return
            else:
                # Log plugin attempted
                raise ValueError("Domain not supported")

    def _parse_url(self, url):
        url_parts = urlparse(url)
        parts = url_parts.path.split("/")

        return url_parts.netloc, RepoDetails(parts[1], parts[2])

    def get_stats(self) -> RepoStatistics:
        return self.plugin.repo_stats()

Plugin Systems in the Wild

  • Django
    • Writing custom middleware (something that hooks into Django's request/response cycle):
      • HttpRequest -> Middlewares -> HttpResponse
  • Flask
  • Pytest

Hook Based Plugins:

  • Identify points where Application can be extended
  • When host program loads, enabled plugins are registered for hooks they care about
  • Hook is triggered, trigger registered functions