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.