diff --git a/pymarc/writer.py b/pymarc/writer.py index 8e29032..7368859 100644 --- a/pymarc/writer.py +++ b/pymarc/writer.py @@ -1,48 +1,247 @@ +import pymarc from pymarc import Record, WriteNeedsRecord +try: + import xml.etree.ElementTree as ET # builtin in Python 2.5 +except ImportError: + import elementtree.ElementTree as ET + +try: + # the json module was included in the stdlib in python 2.6 + # http://docs.python.org/library/json.html + import json +except ImportError: + # simplejson 2.0.9 is available for python 2.4+ + # http://pypi.python.org/pypi/simplejson/2.0.9 + # simplejson 1.7.3 is available for python 2.3+ + # http://pypi.python.org/pypi/simplejson/1.7.3 + import simplejson as json + + class Writer(object): + def __init__(self, file_handle, own_file_handle = True): + self.file_handle = file_handle + self.own_file_handle = own_file_handle + def write(self, record): - pass + if not isinstance(record, Record): + raise WriteNeedsRecord + + def close(self): + """ + Closes the writer. + + If own_file_handle is True, also closes the file handle. + """ + if self.own_file_handle: + self.file_handle.close() + self.file_handle = None + + +class JSONWriter(Writer): + """ + A class for writing records as an array of MARC-in-JSON objects. + + IMPORTANT: You must the close a JSONWriter, + otherwise you will not get valid JSON. + + Simple usage:: + + >>> from pymarc import JSONWriter + + ### writing to a file + >>> writer = JSONWriter(open('file.json','wt')) + >>> writer.write(record) + >>> writer.close() # Important! + + ### writing to a string + >>> string = StringIO() + >>> writer = JSONWriter(string, own_file_handle = False) + >>> writer.write(record) + >>> writer.close() # Important! + >>> print string + """ + + def __init__(self, file_handle, own_file_handle = True): + """ + You need to pass in a text file like object. + + If own_file_handle is True (the default) then the file handle will be + closed when the writer is closed. Otherwise the file handle will be + left open. + """ + super(JSONWriter, self).__init__(file_handle, own_file_handle) + self.write_count = 0 + self.file_handle.write('[') + + def write(self, record): + """ + Writes a record. + """ + Writer.write(self, record) + if self.write_count > 0: + self.file_handle.write(',') + json.dump(record.as_dict(), self.file_handle, separators=(',', ':')) + self.write_count += 1 + + def close(self): + """ + Closes the writer. + + If own_file_handle is True, also closes the file handle. + """ + self.file_handle.write(']') + Writer.close(self) + class MARCWriter(Writer): """ A class for writing MARC21 records in transmission format. - Simple usage: + Simple usage:: + + >>> from pymarc import MARCWriter - from pymarc import MARCWriter + ### writing to a file + >>> writer = MARCWriter(open('file.dat','wb')) + >>> writer.write(record) + >>> writer.close() - ## pass in a file - writer = MARCWriter(file('file.dat','w')) - writer.write(record) + ### writing to a string (Python 2 only) + >>> string = StringIO() + >>> writer = MARCWriter(string) + >>> writer.write(record) + >>> writer.close() + >>> print string - ## use StringIO if you want to write to a string - string = StringIO() - writer = MARCWriter(string) - writer.write(record) - print string + ### writing to memory (Python 3 only) + >>> memory = BytesIO() + >>> writer = MARCWriter(memory) + >>> writer.write(record) + >>> writer.close() """ - def __init__(self, file_handle): + def __init__(self, file_handle, own_file_handle = True): """ - You need to pass in a file like object. + You need to pass in a byte file like object. + + If own_file_handle is True (the default) then the file handle will be + closed when the writer is closed. Otherwise the file handle will be + left open. """ - super(MARCWriter, self).__init__() - self.file_handle = file_handle + super(MARCWriter, self).__init__(file_handle, own_file_handle) def write(self, record): """ Writes a record. """ - if not isinstance(record, Record): - raise WriteNeedsRecord + Writer.write(self, record) self.file_handle.write(record.as_marc()) - def close(self): + +class TextWriter(Writer): + """ + A class for writing records in prettified text MARCMaker format. + + A blank line separates each record. + + Simple usage:: + + >>> from pymarc import TextWriter + + ### writing to a file + >>> writer = TextWriter(open('file.txt','wt')) + >>> writer.write(record) + >>> writer.close() + + ### writing to a string + >>> string = StringIO() + >>> writer = TextWriter(string, own_file_handle = False) + >>> writer.write(record) + >>> writer.close() + >>> print string + """ + + def __init__(self, file_handle, own_file_handle = True): """ - Closes the file. + You need to pass in a text file like object. + + If own_file_handle is True (the default) then the file handle will be + closed when the writer is closed. Otherwise the file handle will be + left open. """ - self.file_handle.close() - self.file_handle = None + super(TextWriter, self).__init__(file_handle, own_file_handle) + self.write_count = 0 + + def write(self, record): + """ + Writes a record. + """ + Writer.write(self, record) + if self.write_count > 0: + self.file_handle.write('\n') + self.file_handle.write(str(record)) + self.write_count += 1 + + +class XMLWriter(Writer): + """ + A class for writing records as a MARCXML collection. + IMPORTANT: You must the close an XMLWriter, otherwise you will not get + a valid XML document. + + Simple usage:: + + >>> from pymarc import XMLWriter + + ### writing to a file + >>> writer = XMLWriter(open('file.xml','wb')) + >>> writer.write(record) + >>> writer.close() # Important! + + ### writing to a string (Python 2 only) + >>> string = StringIO() + >>> writer = XMLWriter(string) + >>> writer.write(record) + >>> writer.close() # Important! + >>> print string + + ### writing to memory (Python 3 only) + >>> memory = BytesIO() + >>> writer = XMLWriter(memory) + >>> writer.write(record) + >>> writer.close() # Important! + """ + + def __init__(self, file_handle, own_file_handle = True): + """ + You need to pass in a binary file like object. + + If own_file_handle is True (the default) then the file handle will be + closed when the writer is closed. Otherwise the file handle will be + left open. + """ + super(XMLWriter, self).__init__(file_handle, own_file_handle) + self.file_handle.write( + b'') + self.file_handle.write( + b'') + + def write(self, record): + """ + Writes a record. + """ + Writer.write(self, record) + node = pymarc.record_to_xml_node(record) + self.file_handle.write(ET.tostring(node, encoding='utf-8')) + + def close(self): + """ + Closes the writer. + + If own_file_handle is True, also closes the file handle. + """ + self.file_handle.write(b'') + Writer.close(self) diff --git a/test/writer.py b/test/writer.py index 782415a..576196c 100644 --- a/test/writer.py +++ b/test/writer.py @@ -1,18 +1,264 @@ import unittest import pymarc import os +import textwrap +from six import BytesIO, StringIO, u, binary_type + +try: + # the json module was included in the stdlib in python 2.6 + # http://docs.python.org/library/json.html + import json +except ImportError: + # simplejson 2.0.9 is available for python 2.4+ + # http://pypi.python.org/pypi/simplejson/2.0.9 + # simplejson 1.7.3 is available for python 2.3+ + # http://pypi.python.org/pypi/simplejson/1.7.3 + import simplejson as json + + +class JSONWriterTest(unittest.TestCase): + + def test_own_file_handle_true(self): + """ + If a JSONWriter is created with own_file_handle = True, then when the + JSONWriter is closed the file handle is also closed. + """ + file_handle = StringIO() + self.assertFalse( + file_handle.closed, + 'The file handle should be open') + writer = pymarc.JSONWriter(file_handle, own_file_handle = True) + self.assertFalse( + file_handle.closed, + 'The file handle should still be open') + writer.close() + self.assertTrue( + file_handle.closed, + 'The file handle should close when the writer closes') + + def test_own_file_handle_false(self): + """ + If a JSONWriter is created with own_file_handle = False, then when the + JSONWriter is closed the file handle is NOT also closed. + """ + file_handle = StringIO() + self.assertFalse( + file_handle.closed, + 'The file handle should be open') + writer = pymarc.JSONWriter(file_handle, own_file_handle = False) + self.assertFalse( + file_handle.closed, + 'The file handle should still be open') + writer.close() + self.assertFalse( + file_handle.closed, + 'The file handle should NOT close when the writer closes') + + def test_writing_0_records(self): + expected = json.loads(r""" + [] + """) + file_handle = StringIO() + try: + writer = pymarc.JSONWriter(file_handle, own_file_handle = False) + writer.close() + actual = json.loads(file_handle.getvalue()) + self.assertEquals(actual, expected) + finally: + file_handle.close() + + def test_writing_empty_record(self): + expected = json.loads(r""" + [ + { + "leader" : " 22 4500", + "fields" : [] + } + ] + """) + file_handle = StringIO() + try: + writer = pymarc.JSONWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + writer.write(record) + writer.close() + actual = json.loads(file_handle.getvalue()) + self.assertEquals(actual, expected) + finally: + file_handle.close() + + def test_writing_1_record(self): + expected = json.loads(r""" + [ + { + "leader" : " 22 4500", + "fields" : [ + { + "100": { + "ind1": "0", + "ind2": "0", + "subfields": [ + { "a": "me" } + ] + } + }, + { + "245": { + "ind1": "0", + "ind2": "0", + "subfields": [ + { "a": "Foo /" }, + { "c": "by me." } + ] + } + } + ] + } + ] + """) + file_handle = StringIO() + try: + writer = pymarc.JSONWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + writer.close() + actual = json.loads(file_handle.getvalue()) + self.assertEquals(actual, expected) + finally: + file_handle.close() + + def test_writing_3_records(self): + expected = json.loads(r""" + [ + { + "leader" : " 22 4500", + "fields" : [ + { + "008": "090227s2009 mau chi d" + }, + { + "100": { + "ind1": "0", + "ind2": "0", + "subfields": [ + { "a": "me" } + ] + } + }, + { + "245": { + "ind1": "0", + "ind2": "0", + "subfields": [ + { "a": "Foo /" }, + { "c": "by me." } + ] + } + } + ] + }, + { + "leader" : " 22 4500", + "fields" : [ + { + "100": { + "ind1": "0", + "ind2": "0", + "subfields": [ + { "a": "me" } + ] + } + }, + { + "245": { + "ind1": "0", + "ind2": "0", + "subfields": [ + { "a": "Foo /" }, + { "c": "by me." } + ] + } + } + ] + }, + { + "leader" : " 22 4500", + "fields" : [ + { + "245": { + "ind1": "0", + "ind2": "0", + "subfields": [ + { "a": "Foo /" }, + { "c": "by me." } + ] + } + } + ] + } + ] + """) + file_handle = StringIO() + try: + writer = pymarc.JSONWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + record.add_field( + pymarc.Field( + '008', + data=u('090227s2009 mau chi d'))) + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + record = pymarc.Record() + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + record = pymarc.Record() + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + writer.close() + actual = json.loads(file_handle.getvalue()) + self.assertEquals(actual, expected) + finally: + file_handle.close() + class MARCWriterTest(unittest.TestCase): def test_write(self): # write a record off to a file - writer = pymarc.MARCWriter(open('test/writer-test.dat', 'wb')) + file_handle = open('test/writer-test.dat', 'wb') + writer = pymarc.MARCWriter(file_handle) record = pymarc.Record() field = pymarc.Field('245', ['0', '0'], ['a', 'foo']) record.add_field(field) writer.write(record) writer.close() + self.assertTrue( + file_handle.closed, + 'The file handle should close when the writer closes') # read it back in reader = pymarc.MARCReader(open('test/writer-test.dat', 'rb')) @@ -22,8 +268,378 @@ def test_write(self): # remove it os.remove('test/writer-test.dat') + def test_own_file_handle_true(self): + """ + If a MARCWriter is created with own_file_handle = True, then when the + MARCWriter is closed the file handle is also closed. + """ + file_handle = BytesIO() + self.assertFalse( + file_handle.closed, + 'The file handle should be open') + writer = pymarc.MARCWriter(file_handle, own_file_handle = True) + self.assertFalse( + file_handle.closed, + 'The file handle should still be open') + writer.close() + self.assertTrue( + file_handle.closed, + 'The file handle should close when the writer closes') + + def test_own_file_handle_false(self): + """ + If a MARCWriter is created with own_file_handle = False, then when the + MARCWriter is closed the file handle is NOT also closed. + """ + file_handle = BytesIO() + self.assertFalse( + file_handle.closed, + 'The file handle should be open') + writer = pymarc.MARCWriter(file_handle, own_file_handle = False) + self.assertFalse( + file_handle.closed, + 'The file handle should still be open') + writer.close() + self.assertFalse( + file_handle.closed, + 'The file handle should NOT close when the writer closes') + + +class TextWriterTest(unittest.TestCase): + + def test_writing_0_records(self): + file_handle = StringIO() + try: + writer = pymarc.TextWriter(file_handle, own_file_handle = False) + writer.close() + self.assertEqual( + file_handle.getvalue(), + '', + 'Nothing should be have been written to the file handle') + finally: + file_handle.close() + + def test_writing_1_record(self): + expected = r""" + =LDR 22 4500 + =100 00$ame + =245 00$aFoo /$cby me. + """ + expected = textwrap.dedent(expected[1:]) + file_handle = StringIO() + try: + writer = pymarc.TextWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + writer.close() + self.assertEquals(file_handle.getvalue(), expected) + finally: + file_handle.close() + + def test_writing_3_records(self): + expected = r""" + =LDR 22 4500 + =008 090227s2009\\\\mau\\\\\\\\\\\\\\\\\chi\d + =100 00$ame + =245 00$aFoo /$cby me. + + =LDR 22 4500 + =100 00$ame + =245 00$aFoo /$cby me. + + =LDR 22 4500 + =245 00$aFoo /$cby me. + """ + expected = textwrap.dedent(expected[1:]) + file_handle = StringIO() + try: + writer = pymarc.TextWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + record.add_field( + pymarc.Field( + '008', + data=u('090227s2009 mau chi d'))) + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + record = pymarc.Record() + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + record = pymarc.Record() + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + writer.close() + self.assertEquals(file_handle.getvalue(), expected) + finally: + file_handle.close() + + def test_writing_empty_record(self): + expected = r""" + =LDR 22 4500 + """ + expected = textwrap.dedent(expected[1:]) + file_handle = StringIO() + try: + writer = pymarc.TextWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + writer.write(record) + self.assertEquals(file_handle.getvalue(), expected) + finally: + file_handle.close() + + def test_own_file_handle_true(self): + """ + If a TextWriter is created with own_file_handle = True, then when the + TextWriter is closed the file handle is also closed. + """ + file_handle = StringIO() + self.assertFalse( + file_handle.closed, + 'The file handle should be open') + writer = pymarc.TextWriter(file_handle, own_file_handle = True) + self.assertFalse( + file_handle.closed, + 'The file handle should still be open') + writer.close() + self.assertTrue( + file_handle.closed, + 'The file handle should close when the writer closes') + + def test_own_file_handle_false(self): + """ + If a TextWriter is created with own_file_handle = False, then when the + TextWriter is closed the file handle is NOT also closed. + """ + file_handle = StringIO() + self.assertFalse( + file_handle.closed, + 'The file handle should be open') + writer = pymarc.TextWriter(file_handle, own_file_handle = False) + self.assertFalse( + file_handle.closed, + 'The file handle should still be open') + writer.close() + self.assertFalse( + file_handle.closed, + 'The file handle should NOT close when the writer closes') + + +class XMLWriterTest(unittest.TestCase): + + def test_writing_0_records(self): + expected = r""" + + + + """ + expected = textwrap.dedent(expected[1:]).replace('\n', '') + if str != binary_type: + expected = expected.encode() + file_handle = BytesIO() + try: + writer = pymarc.XMLWriter(file_handle, own_file_handle = False) + writer.close() + self.assertEquals(file_handle.getvalue(), expected) + finally: + file_handle.close() + + def test_writing_empty_record(self): + expected = r""" + + + + 22 4500 + + + """ + expected = textwrap.dedent(expected[1:]).replace('\n', '') + if str != binary_type: + expected = expected.encode() + file_handle = BytesIO() + try: + writer = pymarc.XMLWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + writer.write(record) + writer.close() + self.assertEquals(file_handle.getvalue(), expected) + finally: + file_handle.close() + + def test_writing_1_record(self): + expected = r""" + + + + 22 4500 + + me + + + Foo / + by me. + + + + """ + expected = textwrap.dedent(expected[1:]).replace('\n', '') + if str != binary_type: + expected = expected.encode() + file_handle = BytesIO() + try: + writer = pymarc.XMLWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + writer.close() + self.assertEquals(file_handle.getvalue(), expected) + finally: + file_handle.close() + + def test_writing_3_records(self): + expected = r""" + + + + 22 4500 + 090227s2009 mau chi d + + me + + + Foo / + by me. + + + + 22 4500 + + me + + + Foo / + by me. + + + + 22 4500 + + Foo / + by me. + + + + """ + expected = textwrap.dedent(expected[1:]).replace('\n', '') + if str != binary_type: + expected = expected.encode() + file_handle = BytesIO() + try: + writer = pymarc.XMLWriter(file_handle, own_file_handle = False) + record = pymarc.Record() + record.add_field( + pymarc.Field( + '008', + data=u('090227s2009 mau chi d'))) + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + record = pymarc.Record() + record.add_field( + pymarc.Field('100', ['0', '0'], ['a', u('me')])) + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + record = pymarc.Record() + record.add_field( + pymarc.Field( + '245', + ['0', '0'], + ['a', u('Foo /'), 'c', u('by me.')])) + writer.write(record) + writer.close() + self.assertEquals(file_handle.getvalue(), expected) + finally: + file_handle.close() + + def test_own_file_handle_true(self): + """ + If a XMLWriter is created with own_file_handle = True, then when the + XMLWriter is closed the file handle is also closed. + """ + file_handle = BytesIO() + self.assertFalse( + file_handle.closed, + 'The file handle should be open') + writer = pymarc.XMLWriter(file_handle, own_file_handle = True) + self.assertFalse( + file_handle.closed, + 'The file handle should still be open') + writer.close() + self.assertTrue( + file_handle.closed, + 'The file handle should close when the writer closes') + + def test_own_file_handle_false(self): + """ + If a XMLWriter is created with own_file_handle = False, then when the + XMLWriter is closed the file handle is NOT also closed. + """ + file_handle = BytesIO() + self.assertFalse( + file_handle.closed, + 'The file handle should be open') + writer = pymarc.XMLWriter(file_handle, own_file_handle = False) + self.assertFalse( + file_handle.closed, + 'The file handle should still be open') + writer.close() + self.assertFalse( + file_handle.closed, + 'The file handle should NOT close when the writer closes') + + def suite(): - test_suite = unittest.makeSuite(MARCWriterTest, 'test') + json_suite = unittest.makeSuite(JSONWriterTest, 'test') + marc_suite = unittest.makeSuite(MARCWriterTest, 'test') + text_suite = unittest.makeSuite(TextWriterTest, 'test') + xml_suite = unittest.makeSuite(XMLWriterTest, 'test') + test_suite = unittest.TestSuite((json_suite, marc_suite, text_suite, xml_suite)) return test_suite if __name__ == '__main__':