Rationale of File System
The mocking made for plugin loader and some common sense gives us following constraints:
- A directory should have a name attribute.
- A directory should have a children attribute.
- The children attribute of directory should have find method that seeks for a file matching to the given name. Note that the implementation of plugin loader presumed that the find method has named parameter "name" for this purpose. I probably should test this case separately but acknowledging the interface used should be enough for now.
- A plugin file (.py) should have classes attribute that contains all classes found in that specific file. The attribute should be a dictionary where key is the name of the class in lowercase and the value is the class.
- Additionally there should be a way to point the file system to some initial directory (ie. the directory of the plugins :) )
Initial Tests and Implementation
The constraints above convert to tests easily. Here are my tests:
And here's the implementation:
As before, the files should go to /placidity (ie. /placidity/file_system.py and /placidity/tests/test_file_system.py).
I had some troubles figuring out how to mock "with". As a result I came up with mock_with_read function that encapsulates it. My solution was heavily inspired by this particular answer at Stack Overflow.
There are a couple of concepts, patching and sentinels, I probably should elaborate on a bit.
The idea is that patching (ie. @patch) allows you can to mock the behavior of some external dependency. In this case I use it to mock Python's native open function and provide the mock with needed extras so I can use it as a context manager.
The sentinels are there just to return unique objects. Sure, I could just use a Mock() object there instead but that's not the point. Suppose line "file = File(sentinel.filepath)". If I provided it a Mock() it would still work but in that case I would miss totally the intent of the parameter. A sentinel helps me to communicate that it functions as a substitute for a file path.
I discussed the "with" issue with the author of Mock and he added some goodies to Mock that make mocking protocol methods such as this much easier. These goodies, including a way to mock function signatures, will make it to the next official release of Mock. Thanks Michael! :) Check out further discussion at his blog.
I'm not entirely happy about the implementation as a File is a Node. Hence it would make sense to derive it from one. Luckily I have implemented one for another project of mine earlier. Let's refactor the code to use it next.
Note that the system would work fine without this step but I think it might be nice to show how one might go about doing something like this.
Refactoring File to Be Derived from a Node
The design of my Node class is quite simple. It provides parents ("inputs") and children ("outputs"). In addition there are methods to walk through the node graph, find, append and remove new nodes as needed.
As the file structure is actually a subset of a node graph, a tree, I implemented a TreeNode encapsulating this observation. The idea is that a TreeNode wraps the parents attribute of a Node and enforces that a TreeNode may contain only one parent at maximum.
In addition a TreeNode provides find method needed by File and various other classes. By default a Node provides find_child and find_parent but clearly find_parent is not needed in this case. So it's enough if TreeNode is able to alias find_child. Parent may be accessed simply by using the parent attribute.
The refactoring took quite a few changes, too many to go through here. Refactored versions of the files I modified may be found here.
Now we have a simple abstraction for the file system that should be compatible with the plugin loader we implemented in the previous part of the series. If everything went fine and I didn't forget something crucial, I suppose I might be able to actually construct an Application in the next part of the series. Till then!
Note that you may find the source code of this part of the series here.