Read my book

I wrote books about Webpack and React. Check them out!

Sunday, December 13, 2009

Placidity - Part 4, Plugin Architecture I

In the previous part of this series we finalized the variable related functionality and implemented a simple "help" command to help the user. I concluded the post by noting the structure of command architecture could use some extra effort. Let's focus on that in this part and come up with a proper plugin architecture.


New Folder Structure

To make it easy for the users to extend and alter the interactive interpreter on demand, it's cleaner to treat each command as a plugin. It should be possible to treat a plugin as a self-contained entity that may be easily shared and just copied into the system. The interpreter should be able to use it just based on the fact that it's located in some predefined directory.

Let's consider this from the viewpoint of Python. The following organization of files should implement the description given above:
  • /placidity
  • (Optional) /placidity/INSTALL
  • (Optional) /placidity/README
  • /placidity/placidity
  • /placidity/placidity/__init__.py
  • /placidity/placidity/interpreter.py
  • /placidity/placidity/tests
  • /placidity/placidity/tests/__init__.py
  • /placidity/placidity/tests/test_interpreter.py
  • /placidity/placidity/commands
  • /placidity/placidity/commands/clean
  • /placidity/placidity/commands/clean/clean.py
  • /placidity/placidity/commands/help/help.py
  • /placidity/placidity/commands/variables/variables.py
Note that I decided not to include __init__.py in the plugin folders. I feel it just adds extra cruft to the system we can manage without for now. You might wonder where the tests go. I think that Python's doctests work adequately in this case so that the test snippet needed may reside within the command modules.

Alternatively I could have just put the the plugins directly into the commands folder. The problem of this solution is that it makes sharing commands harder as you might have command that depends on some specific Python modules. Now you can just store the dependencies in the folder of a command itself. I'm not saying this is a perfect solution as it may easily lead to some duplication but it's good enough for now. Perhaps there needs to be some way to state required modules (ie. รก la pip) to get rid of possible duplication later.

Now that it's clear how the new system should be structured, let's start hacking! It's probably most straightforward to port the commands in the new system first and later glue it with the interpreter.

Implementation of Commands

Let's start out by creating a file structure such as the one described above. The new files may be empty for now. We will migrate the contents of old commands module to the new architecture bit by bit.

To get started with the new "clean", let's prototype desired file contents. I think something along this looks acceptable:

clean.py:


The part at the end seems bit of a kludge to me. It might be nice to implement a proper doctest runner to get rid of it later.

Here are the implementations of the remaining commands, "help" and "variables". Note that I'm using Michael Foord's Mock as my mocking tool. Besides the tool itself, check out his article about mocking.

help.py:


variables.py:


It's important to note that now the interface of the execute method varies depending on the command. This is something we need to take in count in the implementation of the plugin loader.

Cleaning Up

As you may have already noticed the interpreter tests fail due to the new commands package. This is due to conflicting naming with the old commands module (commands.py). Feel free to remove the file, we won't be needing it anymore.

Thanks to our doctests it is possible to tidy up interpreter tests considerably. After removing the command specific parts I ended up with this:

test_interpreter.py:


And here's a minimal implementation of those tests:

interpreter.py:


Summary

So far we have managed to separate commands as plugins of their own. The system is still missing a way to load them into interpreter. I will look into that in the next part of the series. You may find the source code of this part of the series here.