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
- Writing custom middleware (something that hooks into Django's request/response cycle):
- 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