Skip to content
This repository has been archived by the owner on May 7, 2024. It is now read-only.

Commit

Permalink
feat: generic plugin for SupportsPlugin
Browse files Browse the repository at this point in the history
  • Loading branch information
Predeactor committed Dec 3, 2022
1 parent 8ba0343 commit 830a821
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 195 deletions.
5 changes: 1 addition & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,7 @@ Enhancement suggestions are tracked as [GitHub issues](https://github.com/Predea
<!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->

### Your First Code Contribution
<!-- TODO
include Setup of env, IDE and typical getting started instructions?
-->
<!-- TODO include Setup of env, IDE and typical getting started instructions? -->

We use [Poetry](https://python-poetry.org/) to manage our environment of development, it is extremely recommended you use this tool yourself when editing Fabricius source code. This guide will explain you how to configure, edit, push and request changes into Fabricius.

Expand Down
5 changes: 3 additions & 2 deletions fabricius/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from importlib.metadata import version as __package_version
from importlib.metadata import distribution as __dist

__version__ = __package_version("fabricius")
__version__ = __dist("fabricius").version
__author__ = __dist("fabricius").metadata["Author"]
3 changes: 3 additions & 0 deletions fabricius/generator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .errors import AlreadyCommittedError as AlreadyCommittedError
from .errors import NoContentError as NoContentError
from .errors import NoDestinationError as NoDestinationError
14 changes: 6 additions & 8 deletions fabricius/generator/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing_extensions import Self

from fabricius.const import FILE_STATE, Data
from fabricius.const import FILE_STATE, Data, PathStrOrPath
from fabricius.generator.errors import (
AlreadyCommittedError,
NoContentError,
Expand Down Expand Up @@ -105,7 +105,7 @@ def __init__(self, name: str, extension: Optional[str] = None):
self.renderer = PythonFormatRenderer
self.data = {}

def from_file(self, path: str | pathlib.Path) -> Self:
def from_file(self, path: PathStrOrPath) -> Self:
"""
Read the content from a file template.
Expand All @@ -119,7 +119,7 @@ def from_file(self, path: str | pathlib.Path) -> Self:
path : :py:class:`str` or :py:class:`pathlib.Path`
The path of the file template.
"""
path = pathlib.Path(path) if isinstance(path, str) else path
path = pathlib.Path(path).resolve()
self.content = path.read_text()
return self

Expand All @@ -135,21 +135,19 @@ def from_content(self, content: str) -> Self:
self.content = content
return self

def to_directory(self, directory: str | pathlib.Path) -> Self:
def to_directory(self, directory: PathStrOrPath) -> Self:
"""
Set the directory where the file will be saved.
Raises
------
:py:exc:`FileNotFoundError` :
The given directory does not exist. (And recursive is set to False)
:py:exc:`NotADirectory` :
The given path exists but is not a directory.
Parameters
----------
directory : :py:class:`str` or :py:class:`pathlib.Path`
Where the file will be stored. Does not include the file's name.
recursive : :py:class:`bool`
Create the parents folder if not existing. Default to ``True``.
"""
path = pathlib.Path(directory).resolve()
if path.exists() and not path.is_dir():
Expand Down
26 changes: 13 additions & 13 deletions fabricius/generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
from fabricius.interfaces import SupportsPlugin
from fabricius.plugins.generator import GeneratorPlugin

Plugin = cast(GeneratorPlugin, GeneratorPlugin)
# That might seems useless, but this is to trick static type checkers when using "_plugin_call"
# If we use "GeneratorPlugin" directly with "_plugin_call", we will also get the "self" args in
PluginType = cast(GeneratorPlugin, GeneratorPlugin)
# That might seem useless, but this is to trick static type checkers when using "_plugin_call"
# If we use "GeneratorPlugin" directly with "send_to_plugins", we will also get the "self" args in
# the way, by using this variable, we get rid of the "self" and everyone's happy!


class Generator(SupportsPlugin):
class Generator(SupportsPlugin[GeneratorPlugin]):
files: List[FileGenerator]
"""
The list of files to generate with the generator.
Expand Down Expand Up @@ -44,7 +44,7 @@ def add_file(self, name: str, extension: Optional[str] = None) -> FileGenerator:
"""
file = FileGenerator(name, extension)
self.files.append(file)
self.send_to_plugins(Plugin.on_file_add, file=file)
self.send_to_plugins(PluginType.on_file_add, file=file)
return file

def execute(
Expand All @@ -67,18 +67,17 @@ def execute(
Dict[:py:class:`fabricius.generator.file.FileGenerator`, :py:class:`fabricius.generator.file.CommitResult`] :
A dict containing a file generator and its commit result.
In case the value is ``None``, this mean that the file was not successfully saved to
the disk (Already commited, file already exists, etc.).
the disk (Already committed, file already exists, etc.).
"""
result: Dict[FileGenerator, Optional[GeneratorCommitResult]] = {}

self.send_to_plugins(Plugin.before_execution)
self.send_to_plugins(PluginType.before_execution)

for file in self.files:

try:
self.send_to_plugins(Plugin.before_file_commit, file=file)
self.send_to_plugins(PluginType.before_file_commit, file=file)
file_result = file.commit(overwrite=allow_overwrite, dry_run=dry_run)
self.send_to_plugins(Plugin.after_file_commit, file=file)
self.send_to_plugins(PluginType.after_file_commit, file=file)

result[file] = file_result

Expand All @@ -88,10 +87,11 @@ def execute(
FileExistsError,
AlreadyCommittedError,
) as error:
self.send_to_plugins(Plugin.on_commit_fail, file=file, exception=error)
self.send_to_plugins(PluginType.on_commit_fail, file=file, exception=error)
# Was there a real reason to separate this error handling?

except Exception as error:
self.send_to_plugins(Plugin.on_commit_fail, file=file, exception=error)
self.send_to_plugins(PluginType.on_commit_fail, file=file, exception=error)

self.send_to_plugins(Plugin.after_execution, results=result)
self.send_to_plugins(PluginType.after_execution, results=result)
return result
77 changes: 58 additions & 19 deletions fabricius/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import Any, Callable, List, Optional, Type, ParamSpec, cast
from typing import Any, Callable, Generic, List, Optional, Type, ParamSpec, TypeVar, cast

from fabricius.errors import PluginConnectionError


class BasePlugin:
"""
The base class for all plugins.
"""

def setup(self) -> Any:
"""
Called when the plugin has been connected to the class.
Expand All @@ -18,20 +22,23 @@ def teardown(self) -> Any:


_P = ParamSpec("_P")
_PT = TypeVar("_PT", bound=BasePlugin)


class SupportsPlugin(Generic[_PT]):
"""
Define a class that can support plugin addition.
"""

class SupportsPlugin:
plugins: List[BasePlugin]
plugins: List[_PT]
"""
A list of plugins to use with the class.
"""

def __init__(self) -> None:
self.plugins = []

def __search_connected_plugin(
self, plugin: Type[BasePlugin]
) -> Optional[BasePlugin]:
def _search_connected_plugin(self, plugin: Type[_PT]) -> Optional[_PT]:
"""
Look up connected plugins and return a plugin if a same instance of the given plugin is
found.
Expand All @@ -50,7 +57,7 @@ def __search_connected_plugin(
if isinstance(connected_plugin, plugin):
return connected_plugin

def connect_plugin(self, plugin: BasePlugin, *, force_append: bool = False):
def connect_plugin(self, plugin: _PT, *, force_append: bool = False):
"""
Add (Connect) a plugin to the class.
Expand All @@ -61,7 +68,7 @@ def connect_plugin(self, plugin: BasePlugin, *, force_append: bool = False):
force_append : :py:class:`bool`
In case the plugin's setup did not succeed (Thrown an exception), it will not be
added to the class's plugins. Setting this value to ``True`` will always append
the plugin to the class's plugins even when it fails to setup.
the plugin to the class's plugins even when it fails to set up.
This will also avoid raising ``PluginConnectionError`` when failing.
Raises
Expand All @@ -75,13 +82,15 @@ def connect_plugin(self, plugin: BasePlugin, *, force_append: bool = False):
:py:class:`bool` :
If the plugin was successfully added.
"""
if self.__search_connected_plugin(plugin.__class__):
if self._search_connected_plugin(plugin.__class__):
raise PluginConnectionError(
plugin.__class__.__name__, "Plugin is already connected to the generator."
)

try:
plugin.setup()
except NotImplementedError:
pass
except Exception as error:
if not force_append:
raise PluginConnectionError(
Expand All @@ -91,7 +100,7 @@ def connect_plugin(self, plugin: BasePlugin, *, force_append: bool = False):
self.plugins.append(plugin)
return True

def disconnect_plugin(self, plugin: Type[BasePlugin] | BasePlugin) -> bool:
def disconnect_plugin(self, plugin: _PT | Type[_PT]) -> bool:
"""
Remove (Disconnect) a plugin to this generator.
Expand All @@ -100,42 +109,72 @@ def disconnect_plugin(self, plugin: Type[BasePlugin] | BasePlugin) -> bool:
Parameters
----------
plugin : :py:class:`fabricius.interfaces.BasePlugin`
plugin : Type of :py:class:`fabricius.interfaces.BasePlugin` or :py:class:`fabricius.interfaces.BasePlugin`
The plugin to disconnect.
Returns
-------
:py:class:`bool` :
True if the plugin was successfully removed.
"""
# Obtain the non-initialized class of plugin
plugin_class = plugin if isinstance(plugin, type) else plugin.__class__
if connected_plugin := self.__search_connected_plugin(plugin_class):

if connected_plugin := self._search_connected_plugin(plugin_class):
try:
connected_plugin.teardown()
finally:
self.plugins.remove(connected_plugin)
return True
return True
return False

def send_to_plugins(
self, method: Callable[_P, None], *args: _P.args, **kwargs: _P.kwargs
self,
method: Callable[_P, None],
ignore_missing_method: bool = False,
*args: _P.args,
**kwargs: _P.kwargs,
) -> bool:
"""Call a given method for all connected plugins.
Parameters
----------
method : Callable[_P, None]
The method to call.
ignore_missing_method : bool
If :py:exc:`NotImplementedError` should be raised, see the ``Raises`` part.
*args : _P.args
The arguments to pass.
**kwargs : _P.kwargs
The positionals arguments to pass.
The positional arguments to pass.
Raises
------
:py:exc:`NotImplementedError` :
Raised if ``ignore_missing_method`` is set to ``False`` and that one of the plugins
does not have defined the method in its class.
:py:exc:`Exception` :
Any exception raised by the given method.
Returns
-------
:py:class:`bool` :
If the function has been called with success.
Return `False` when the function has raised `NotImplementedError`, else `True`
"""
function_name = method.__name__
for plugin in self.plugins:
function = cast(Callable[_P, None], getattr(plugin, function_name))
to_call = cast(
List[Callable[_P, None]],
[getattr(plugin, method.__name__, None) for plugin in self.plugins],
)

if (not ignore_missing_method) and any(func is None for func in to_call):
raise NotImplementedError(
f'All plugins does not have the "{str(method)}" method defined.'
)

for func in to_call:
try:
function(*args, **kwargs)
func(*args, **kwargs)
except NotImplementedError:
return False
return True
2 changes: 1 addition & 1 deletion fabricius/plugins/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class GeneratorPlugin(BasePlugin):
"""
A plugin to plug to the :py:class:`fabricius.generator.generator.Generator` class.
You can edit the methods of the class and they'll be run according to their description.
You can edit the methods of the class, and they'll be run according to their description.
"""

def on_file_add(self, file: FileGenerator) -> Any:
Expand Down
14 changes: 9 additions & 5 deletions fabricius/plugins/generator_rich.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class GeneratorRichPlugin(GeneratorPlugin):
verbose: bool
"""
Indicate if the plugin should print more informations than usual.
Indicate if the plugin should print more information than usual.
"""

console: Console
Expand All @@ -34,22 +34,26 @@ def setup(self):

def teardown(self):
if self.verbose:
self.console.print("[yellow]:put_litter_in_its_place: Rich plugin is being disconnected from the generator.")
self.console.print(
"[yellow]:put_litter_in_its_place: Rich plugin is being disconnected from the generator."
)

def on_file_add(self, file: FileGenerator):
if self.verbose:
self.console.print(f"[green]:heavy_plus_sign: File added: [underline]{file.name}")

def before_execution(self):
if self.verbose:
self.console.print("[yellow]:stopwatch: \".execute\" was called. Generator is about to run!")
self.console.print(
'[yellow]:stopwatch: ".execute" was called. Generator is about to run!'
)

def before_file_commit(self, file: FileGenerator):
if self.verbose:
self.console.print(f":mag: {file.name} is about to be commited!")
self.console.print(f":mag: {file.name} is about to be committed!")

def after_file_commit(self, file: FileGenerator):
self._print_column("COMMITED", file.name, "green")
self._print_column("COMMITTED", file.name, "green")

def on_commit_fail(self, file: FileGenerator, exception: Exception):
self._print_column("FAILURE", f"{file.name} is a failure!", "red")
Expand Down
4 changes: 4 additions & 0 deletions fabricius/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Utilities shipped with Fabricius.
"""

import inflection


Expand Down

0 comments on commit 830a821

Please sign in to comment.