Basic methods that need to be implemented by a file-like object are:
- read([size]) - read at most size bytes from the file.
- readLine([size]) - read one entire line from the file.
- write(str) - write a string to the file.
- close() - close the file.
Ok, so why should you use file-like objects in your code?
Let's take a look at the ConfigParser class which can be used to read/write configuration data. You can expect to find methods for reading and writing configuration data using files but the class does also include support for file-like objects, which is great. For example it's very easy to write unittests for this class, thanks to the support of file-like objects, without having to create real files with test data. To create a file-like object from a string we can use the StringIO module. The module have both read and write support.
Here's some code:
import unittest import StringIO import ConfigParser class ConfigParserTest(unittest.TestCase): def testHasSection(self): """ has_section() should return True if the section exists and False if is doesn't. """ data = StringIO.StringIO("[My section]\nSetting=1\n") config = ConfigParser.ConfigParser() config.readfp(data) self.assertTrue(config.has_section("My section")) self.assertFalse(config.has_section("Not a section")) def testMissingHeader(self): """ readfp() should raise MissingSectionHeaderError when parsing a file without section headers. """ data = StringIO.StringIO("\nSetting=1\n") config = ConfigParser.ConfigParser() self.assertRaises(ConfigParser.MissingSectionHeaderError, config.readfp, data) if __name__ == "__main__": unittest.main()And a simple write configuration test:
import unittest import StringIO import ConfigParser class WriteConfigTest(unittest.TestCase): def testWriteConfig(self): """ Test write a simple config """ data = StringIO.StringIO() config = ConfigParser.ConfigParser() config.add_section("My Section") config.set("My Section", "my_option", "my_value") config.write(data) expected = "[My Section]\nmy_option = my_value\n\n" self.assertEquals(expected, data.getvalue()) if __name__ == "__main__": unittest.main()These unittests are of course only examples and do not fully test the ConfigParser class.
It's easy to see that file-like objects are very handy and fits well in frameworks and APIs. There are of course many advantages of using an abstraction when reading/writing data, some of them are:
- Implementations do not depend on real files, socket, etc, only on a file-like object.
- Easier to fake data in unittests.
- Caller responsible of opening/closing the resource.
- Less error-handling in framework code, delegate exceptions to the caller.
I've implemented a shoutcast playlist parser, plsparser, using the ConfigParser class. The parser is implemented as a Python generator function. Actually, I might do a post about generator functions someday.