import pytest
from mrrc import Record, Field, Leader, MARCReader, MARCWriter, Subfield, ControlField, Indicators
import io
import json
from pathlib import Path
TEST_DATA_DIR = Path(__file__).parent.parent / 'data'
def create_field(tag, ind1='0', ind2='0', **subfields):
field = Field(tag, ind1, ind2)
for code, value in subfields.items():
field.add_subfield(code, value)
return field
class TestRecordCreation:
def test_create_empty_record(self):
leader = Leader()
record = Record(leader)
assert record is not None
assert len(record.fields()) == 0
def test_record_with_leader(self):
leader = Leader()
leader.record_type = 'c'
leader.bibliographic_level = 'd'
record = Record(leader)
assert record.leader().record_type == 'c'
assert record.leader().bibliographic_level == 'd'
def test_record_equality(self):
leader1 = Leader()
record1 = Record(leader1)
record1.add_control_field('001', 'test-id')
leader2 = Leader()
record2 = Record(leader2)
record2.add_control_field('001', 'test-id')
assert record1 == record2
class TestRecordFieldOperations:
def test_add_single_field(self):
record = Record(Leader())
field = create_field('245', '1', '0', a='Test Title')
record.add_field(field)
retrieved = record.get_field('245')
assert retrieved is not None
def test_add_multiple_fields(self):
record = Record(Leader())
for i in range(3):
field = create_field('650', ' ', '0', a=f'Subject {i}')
record.add_field(field)
fields = record.get_fields('650')
assert len(fields) == 3
def test_add_control_field(self):
record = Record(Leader())
record.add_control_field('001', '12345')
record.add_control_field('003', 'ABC')
assert record.control_field('001') == '12345'
assert record.control_field('003') == 'ABC'
def test_control_field_dict_access(self):
record = Record(Leader())
record.add_control_field('001', '12345')
record.add_control_field('003', 'DLC')
field_001 = record['001']
assert isinstance(field_001, Field)
assert field_001.data == '12345'
assert field_001.tag == '001'
def test_control_field_value_property(self):
record = Record(Leader())
record.add_control_field('005', '20210315120000.0')
assert record['005'].data == '20210315120000.0'
def test_control_field_backward_compat(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
assert record['001'].data == record.control_field('001')
assert record['001'].data == 'test-id'
def test_missing_control_field_raises_keyerror(self):
record = Record(Leader())
with pytest.raises(KeyError):
record['001']
with pytest.raises(KeyError):
record['008']
def test_get_nonexistent_field(self):
record = Record(Leader())
field = record.get_field('999')
assert field is None
def test_get_all_fields(self):
record = Record(Leader())
record.add_field(create_field('245', '1', '0', a='Title'))
record.add_field(create_field('650', ' ', '0', a='Subject'))
all_fields = record.fields()
assert len(all_fields) >= 2
def test_remove_field(self):
record = Record(Leader())
field = create_field('245', '1', '0', a='Title')
record.add_field(field)
assert record.get_field('245') is not None
record.remove_field('245')
assert record.get_field('245') is None
class TestFieldSubfieldOperations:
def test_field_creation_with_indicators(self):
field = Field('245', '1', '0')
assert field.tag == '245'
def test_add_subfield(self):
field = Field('245', '1', '0')
field.add_subfield('a', 'Title')
field.add_subfield('b', 'Subtitle')
assert len(field.subfields()) == 2
def test_multiple_subfields_same_code(self):
field = Field('300', ' ', ' ')
field.add_subfield('a', '256 pages')
field.add_subfield('a', '24 cm')
assert len(field.subfields()) >= 2
def test_subfield_access(self):
field = create_field('245', '1', '0', a='Title', b='Subtitle', c='Creator')
subfield_codes = [sf.code for sf in field.subfields()]
assert 'a' in subfield_codes
assert 'b' in subfield_codes
assert 'c' in subfield_codes
def test_field_getitem_returns_value(self):
field = create_field('245', '1', '0', a='Title')
assert field['a'] == 'Title'
def test_field_getitem_returns_none_for_missing(self):
field = create_field('245', '1', '0', a='Title')
assert field['z'] is None
def test_field_getitem_with_multiple_same_code(self):
field = Field('300', ' ', ' ')
field.add_subfield('a', '256 pages')
field.add_subfield('a', '24 cm')
assert field['a'] == '256 pages'
def test_field_indicators_tuple_access(self):
field = Field('245', '1', '0')
indicators = field.indicators
assert isinstance(indicators, Indicators)
assert indicators[0] == '1'
assert indicators[1] == '0'
def test_field_indicators_unpacking(self):
field = Field('245', '1', '0')
ind1, ind2 = field.indicators
assert ind1 == '1'
assert ind2 == '0'
def test_field_indicators_backward_compat(self):
field = Field('245', '1', '0')
assert field.indicator1 == field.indicators[0]
assert field.indicator2 == field.indicators[1]
def test_field_indicators_setter(self):
field = Field('245', '0', '0')
field.indicators = Indicators('1', '4')
assert field.indicator1 == '1'
assert field.indicator2 == '4'
field.indicators = ('1', '0')
assert field.indicator1 == '1'
assert field.indicator2 == '0'
class TestConvenienceMethods:
def test_title(self):
record = Record(Leader())
record.add_field(create_field('245', '1', '0', a='Test Title'))
assert record.title == 'Test Title'
def test_author(self):
record = Record(Leader())
record.add_field(create_field('100', '1', ' ', a='Author, Test'))
assert 'Author' in record.author
def test_isbn(self):
record = Record(Leader())
record.add_field(create_field('020', ' ', ' ', a='0201616165'))
assert record.isbn == '0201616165'
def test_issn(self):
record = Record(Leader())
record.add_field(create_field('022', ' ', ' ', a='0028-0836'))
assert record.issn == '0028-0836'
def test_publisher(self):
record = Record(Leader())
record.add_field(create_field('260', ' ', ' ', b='Test Publisher'))
assert 'Publisher' in record.publisher or 'Test' in record.publisher
def test_subjects(self):
record = Record(Leader())
for i in range(3):
record.add_field(create_field('650', ' ', '0', a=f'Subject {i}'))
subjects = record.subjects
assert len(subjects) == 3
def test_location(self):
record = Record(Leader())
record.add_field(create_field('852', ' ', ' ', a='Main Library'))
locations = record.location
assert 'Main Library' in locations
def test_notes(self):
record = Record(Leader())
record.add_field(create_field('500', ' ', ' ', a='General note'))
notes = record.notes
assert 'General note' in notes
def test_series(self):
record = Record(Leader())
record.add_field(create_field('490', ' ', ' ', a='Series Name'))
series = record.series
assert series is not None
def test_physical_description(self):
record = Record(Leader())
record.add_field(create_field('300', ' ', ' ', a='256 pages'))
phys_desc = record.physical_description
assert '256' in phys_desc or phys_desc is not None
def test_uniform_title(self):
record = Record(Leader())
record.add_field(create_field('130', ' ', '0', a='Uniform Title'))
uniform = record.uniform_title
assert 'Uniform' in uniform
def test_sudoc(self):
record = Record(Leader())
record.add_field(create_field('086', ' ', ' ', a='I 19.2:En 3'))
sudoc = record.sudoc
assert sudoc == 'I 19.2:En 3'
def test_issn_title(self):
record = Record(Leader())
record.add_field(create_field('222', ' ', ' ', a='Key Title'))
issn_title = record.issn_title
assert 'Key Title' in issn_title
def test_pubyear(self):
record = Record(Leader())
record.add_field(create_field('260', ' ', ' ', c='2023'))
year = record.pubyear
assert year == '2023'
class TestRecordSerialization:
def test_to_json(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
record.add_field(create_field('245', '1', '0', a='Title'))
json_str = record.to_json()
assert json_str is not None
assert 'test-id' in json_str or 'Title' in json_str
def test_to_json_valid_json(self):
record = Record(Leader())
record.add_field(create_field('245', '1', '0', a='Title'))
json_str = record.to_json()
try:
data = json.loads(json_str)
assert isinstance(data, (dict, list))
except json.JSONDecodeError:
pytest.fail("to_json() did not return valid JSON")
def test_to_xml(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
xml_str = record.to_xml()
assert xml_str is not None
assert '<' in xml_str
def test_to_dublin_core(self):
record = Record(Leader())
record.add_field(create_field('245', '1', '0', a='Test Title'))
dc = record.to_dublin_core()
assert isinstance(dc, dict)
assert 'title' in dc or len(dc) > 0
class TestRecordTypeDetection:
def test_is_book(self):
leader = Leader()
leader.record_type = 'a'
leader.bibliographic_level = 'm'
record = Record(leader)
assert record.is_book()
def test_is_serial(self):
leader = Leader()
leader.bibliographic_level = 's'
record = Record(leader)
assert record.is_serial()
def test_is_music(self):
leader = Leader()
leader.record_type = 'c'
record = Record(leader)
assert record.is_music()
def test_is_audiovisual(self):
leader = Leader()
leader.record_type = 'g'
record = Record(leader)
assert record.is_audiovisual()
class TestMARCReaderWriter:
@pytest.fixture
def sample_record(self):
record = Record(Leader())
record.add_control_field('001', '12345')
record.add_field(create_field('245', '1', '0', a='Test Title', b='Subtitle'))
record.add_field(create_field('100', '1', ' ', a='Author, Test'))
record.add_field(create_field('650', ' ', '0', a='Subject'))
return record
def test_reader_creation(self, fixture_1k):
data = io.BytesIO(fixture_1k)
reader = MARCReader(data)
assert reader is not None
def test_reader_iteration(self, fixture_1k):
data = io.BytesIO(fixture_1k)
reader = MARCReader(data)
count = 0
while record := reader.read_record():
assert record is not None
count += 1
if count >= 3:
break
assert count > 0
class TestEdgeCases:
def test_empty_record_serialization(self):
record = Record(Leader())
json_str = record.to_json()
assert json_str is not None
def test_record_with_many_fields(self):
record = Record(Leader())
for i in range(20):
record.add_field(create_field('650', ' ', '0', a=f'Subject {i}'))
subjects = record.subjects
assert len(subjects) == 20
def test_field_with_many_subfields(self):
field = Field('300', ' ', ' ')
for i in range(10):
field.add_subfield('a', f'Value {i}')
assert len(field.subfields()) == 10
def test_special_characters_in_subfields(self):
field = create_field('245', '1', '0',
a='Title with "quotes"',
b="Subtitle with 'apostrophes'")
assert len(field.subfields()) == 2
class TestFormatConversions:
def test_marcjson_roundtrip(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
record.add_field(create_field('245', '1', '0', a='Title'))
marcjson = record.to_marcjson()
assert marcjson is not None
assert len(marcjson) > 0
class TestFieldCreation:
def test_field_subfields_created(self):
field = Field('245', '0', '1')
field.add_subfield('a', 'Title')
field.add_subfield('b', 'Subtitle')
assert len(field.subfields()) == 2
def test_field_indicators(self):
field = Field('245', '1', '0')
field.add_subfield('a', 'Test Title')
assert field.indicator1 == '1'
assert field.indicator2 == '0'
def test_field_reassign_indicators(self):
field = Field('245', '0', '1')
field.indicator1 = '1'
field.indicator2 = '0'
assert field.indicator1 == '1'
assert field.indicator2 == '0'
def test_field_subfield_get_multiple(self):
field = Field('650', ' ', '0')
field.add_subfield('a', 'First Subject')
field.add_subfield('v', 'Subdivision')
result = field.subfields_by_code('a')
assert 'First Subject' in result
def test_field_add_subfield(self):
field = Field('245', '0', '1')
field.add_subfield('a', 'foo')
field.add_subfield('b', 'bar')
subfields = field.subfields()
assert len(subfields) == 2
assert subfields[0].value == 'foo'
def test_field_is_subject_field(self):
subject_field = Field('650', ' ', '0')
subject_field.add_subfield('a', 'Python')
title_field = Field('245', '1', '0')
title_field.add_subfield('a', 'Title')
assert subject_field.is_subject_field()
assert not title_field.is_subject_field()
class TestRecordAdvanced:
def test_record_add_field(self):
record = Record(Leader())
field = Field('245', '1', '0')
field.add_subfield('a', 'Python')
field.add_subfield('c', 'Guido')
record.add_field(field)
assert field in record.fields()
def test_record_quick_access(self):
record = Record(Leader())
title = Field('245', '1', '0')
title.add_subfield('a', 'Python')
record.add_field(title)
assert record['245'] == title
def test_record_getitem_missing_tag(self):
record = Record(Leader())
with pytest.raises(KeyError):
record['999']
with pytest.raises(KeyError):
record['245']
def test_record_membership(self):
record = Record(Leader())
title = Field('245', '1', '0')
title.add_subfield('a', 'Python')
record.add_field(title)
assert '245' in record
assert '999' not in record
def test_record_get_fields_multi(self):
record = Record(Leader())
subject1 = Field('650', ' ', '0')
subject1.add_subfield('a', 'Programming')
subject2 = Field('651', ' ', '0')
subject2.add_subfield('a', 'Computer Science')
record.add_field(subject1)
record.add_field(subject2)
fields = record.get_fields('650', '651')
assert len(fields) == 2
def test_record_remove_field(self):
record = Record(Leader())
field = Field('245', '1', '0')
field.add_subfield('a', 'Title')
record.add_field(field)
assert '245' in record
record.remove_field(field)
assert record.get_field('245') is None
class TestReaderWriter:
def test_reader_from_file(self):
test_file = TEST_DATA_DIR / 'simple_book.mrc'
reader = MARCReader(test_file)
record = next(reader)
assert record is not None
assert len(record.fields()) > 0
def test_reader_iteration(self):
test_file = TEST_DATA_DIR / 'simple_book.mrc'
reader = MARCReader(test_file)
count = 0
for record in reader:
count += 1
assert record is not None
assert count > 0
def test_writer_to_bytes(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
field = Field('245', '1', '0')
field.add_subfield('a', 'Test Title')
record.add_field(field)
output = io.BytesIO()
writer = MARCWriter(output)
writer.write(record)
written_bytes = output.getvalue()
assert len(written_bytes) > 0
def test_roundtrip_record(self):
original = Record(Leader())
original.add_control_field('001', 'test-123')
field = Field('245', '1', '0')
field.add_subfield('a', 'Test Title')
original.add_field(field)
output = io.BytesIO()
writer = MARCWriter(output)
writer.write(original)
output.seek(0)
reader = MARCReader(output)
read_record = next(reader)
assert read_record is not None
assert read_record.control_field('001') == 'test-123'
class TestLeader:
def test_leader_defaults(self):
leader = Leader()
assert leader is not None
def test_leader_record_type(self):
leader = Leader()
leader.record_type = 'a'
assert leader.record_type == 'a'
def test_leader_bibliographic_level(self):
leader = Leader()
leader.bibliographic_level = 'c'
assert leader.bibliographic_level == 'c'
def test_leader_encoding_level(self):
leader = Leader()
leader.encoding_level = '4'
assert leader.encoding_level == '4'
def test_leader_descriptor_cataloging_form(self):
leader = Leader()
leader.descriptive_cataloging_form = 'c'
assert leader.descriptive_cataloging_form == 'c'
def test_leader_multipart_resource_record_level(self):
leader = Leader()
leader.multipart_resource_record_level = 'a'
assert leader.multipart_resource_record_level == 'a'
def test_leader_single_position_access(self):
leader = Leader()
leader.record_status = 'c'
assert leader[5] == 'c'
def test_leader_slice_access(self):
leader = Leader()
record_len_str = leader[0:5]
assert isinstance(record_len_str, str)
assert len(record_len_str) == 5
def test_leader_setitem_by_position(self):
leader = Leader()
leader[5] = 'a' assert leader[5] == 'a'
assert leader.record_status == 'a'
def test_leader_position_and_property_stay_in_sync(self):
leader = Leader()
leader.record_status = 'd'
assert leader[5] == 'd'
leader[6] = 'a'
assert leader.record_type == 'a'
def test_leader_get_valid_values(self):
values = Leader.get_valid_values(5)
assert values is not None
assert 'a' in values
assert 'c' in values
assert 'd' in values
assert 'n' in values
assert 'p' in values
values = Leader.get_valid_values(6)
assert values is not None
assert 'a' in values
assert 'm' in values
values = Leader.get_valid_values(7)
assert values is not None
assert 'm' in values
assert 's' in values
values = Leader.get_valid_values(17)
assert values is not None
assert ' ' in values
assert '1' in values
values = Leader.get_valid_values(18)
assert values is not None
assert 'a' in values
values = Leader.get_valid_values(0)
assert values is None
def test_leader_is_valid_value(self):
assert Leader.is_valid_value(5, 'a') is True
assert Leader.is_valid_value(5, 'c') is True
assert Leader.is_valid_value(5, 'x') is False
assert Leader.is_valid_value(6, 'a') is True
assert Leader.is_valid_value(6, 'm') is True
assert Leader.is_valid_value(6, 'z') is False
assert Leader.is_valid_value(0, '0') is True
assert Leader.is_valid_value(0, 'x') is True
def test_leader_get_value_description(self):
desc = Leader.get_value_description(5, 'a')
assert desc is not None
assert 'Increase in encoding level' in desc
desc = Leader.get_value_description(5, 'c')
assert desc is not None
assert 'Corrected or revised' in desc
desc = Leader.get_value_description(5, 'x')
assert desc is None
desc = Leader.get_value_description(6, 'a')
assert desc is not None
assert 'Language material' in desc
desc = Leader.get_value_description(0, '5')
assert desc is None
class TestEncoding:
def test_utf8_record_creation(self):
record = Record(Leader())
field = Field('245', '1', '0')
field.add_subfield('a', 'Rū Harison no wārudo') record.add_field(field)
assert record.get_field('245') is not None
def test_special_characters(self):
record = Record(Leader())
field = Field('650', ' ', '0')
field.add_subfield('a', 'Müller') field.add_subfield('a', 'Café') record.add_field(field)
assert record.get_field('650') is not None
def test_encoding_to_marc(self):
record = Record(Leader())
field = Field('245', '1', '0')
field.add_subfield('a', 'Test')
record.add_field(field)
encoded = record.to_marc21()
assert encoded is not None
assert record.as_marc() == encoded
class TestRecordBinarySerialization:
def test_as_marc_returns_bytes(self):
record = Record(fields=[
Field('245', '1', '0', subfields=[Subfield('a', 'Test Title')]),
])
record.add_control_field('001', 'test-id')
result = record.as_marc()
assert isinstance(result, bytes)
assert len(result) > 0
def test_as_marc21_alias(self):
record = Record(fields=[
Field('245', '1', '0', subfields=[Subfield('a', 'Test')]),
])
record.add_control_field('001', 'test-id')
assert record.as_marc() == record.as_marc21()
def test_as_marc_roundtrip(self):
record = Record(fields=[
Field('245', '1', '0', subfields=[Subfield('a', 'Roundtrip Test')]),
])
record.add_control_field('001', 'rt-001')
marc_bytes = record.as_marc()
reader = MARCReader(io.BytesIO(marc_bytes))
recovered = next(reader)
assert recovered.title == 'Roundtrip Test'
class TestPymarcJsonSchema:
def test_as_dict_structure(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
record.add_field(Field('245', '1', '0', subfields=[
Subfield('a', 'Title'),
Subfield('c', 'Author'),
]))
d = record.as_dict()
assert 'leader' in d
assert 'fields' in d
assert isinstance(d['fields'], list)
def test_as_dict_control_field_format(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
d = record.as_dict()
cf = None
for f in d['fields']:
if '001' in f:
cf = f
break
assert cf == {'001': 'test-id'}
def test_as_dict_data_field_format(self):
record = Record(fields=[
Field('245', '1', '0', subfields=[
Subfield('a', 'Title'),
Subfield('c', 'Author'),
]),
])
d = record.as_dict()
df = None
for f in d['fields']:
if '245' in f:
df = f
break
assert df is not None
inner = df['245']
assert inner['ind1'] == '1'
assert inner['ind2'] == '0'
assert isinstance(inner['subfields'], list)
assert inner['subfields'][0] == {'a': 'Title'}
assert inner['subfields'][1] == {'c': 'Author'}
def test_as_dict_duplicate_subfield_codes_preserved(self):
record = Record(fields=[
Field('650', ' ', '0', subfields=[
Subfield('a', 'Topic 1'),
Subfield('a', 'Topic 2'),
]),
])
d = record.as_dict()
df = None
for f in d['fields']:
if '650' in f:
df = f
break
sfs = df['650']['subfields']
assert len(sfs) == 2
assert sfs[0] == {'a': 'Topic 1'}
assert sfs[1] == {'a': 'Topic 2'}
def test_as_json_returns_string(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
result = record.as_json()
assert isinstance(result, str)
parsed = json.loads(result)
assert 'leader' in parsed
def test_as_json_kwargs_forwarded(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
result = record.as_json(indent=2)
assert '\n' in result
def test_to_json_unchanged(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
native = record.to_json()
pymarc_compat = record.as_json()
assert json.loads(native) != json.loads(pymarc_compat)
class TestSerialization:
def test_json_serialization(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
field = Field('245', '1', '0')
field.add_subfield('a', 'Title')
record.add_field(field)
json_str = record.to_json()
assert json_str is not None
parsed = json.loads(json_str)
assert parsed is not None
def test_xml_serialization(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
field = Field('245', '1', '0')
field.add_subfield('a', 'Title')
record.add_field(field)
xml_str = record.to_xml()
assert xml_str is not None
assert '<' in xml_str
def test_dublin_core_serialization(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
field = Field('245', '1', '0')
field.add_subfield('a', 'Title')
record.add_field(field)
dc_xml = record.to_dublin_core()
assert dc_xml is not None
class TestConstructorKwargs:
def test_field_with_indicators_kwarg(self):
field = Field('245', indicators=['1', '0'])
assert field.indicator1 == '1'
assert field.indicator2 == '0'
def test_field_with_subfields_kwarg(self):
field = Field('245', '1', '0', subfields=[Subfield('a', 'Test Title')])
assert field['a'] == 'Test Title'
assert len(field.subfields()) == 1
def test_field_with_indicators_and_subfields(self):
field = Field('245', indicators=['1', '0'], subfields=[
Subfield('a', 'Pragmatic Programmer'),
Subfield('c', 'Hunt and Thomas'),
])
assert field.indicator1 == '1'
assert field.indicator2 == '0'
assert field['a'] == 'Pragmatic Programmer'
assert field['c'] == 'Hunt and Thomas'
assert len(field.subfields()) == 2
def test_record_with_fields_kwarg(self):
title = Field('245', '1', '0', subfields=[Subfield('a', 'My Book')])
author = Field('100', '1', ' ', subfields=[Subfield('a', 'Doe, John')])
record = Record(fields=[title, author])
assert record.title == 'My Book'
assert record.get_field('100') is not None
def test_full_inline_construction(self):
record = Record(fields=[
Field('245', indicators=['0', '1'], subfields=[
Subfield('a', 'Pragmatic Programmer'),
]),
Field('100', '1', ' ', subfields=[
Subfield('a', 'Hunt, Andrew'),
]),
Field('650', ' ', '0', subfields=[
Subfield('a', 'Computer programming'),
]),
])
assert record.title == 'Pragmatic Programmer'
assert len(record.get_fields('650')) == 1
def test_field_backward_compat_positional_indicators(self):
field = Field('245', '0', '1')
assert field.indicator1 == '0'
assert field.indicator2 == '1'
def test_record_backward_compat_no_args(self):
record = Record()
assert record is not None
assert len(record.fields()) == 0
def test_record_with_leader_and_fields(self):
leader = Leader()
leader.record_type = 'a'
leader.bibliographic_level = 'm'
record = Record(leader, fields=[
Field('245', '1', '0', subfields=[Subfield('a', 'Title')]),
])
assert record.leader().record_type == 'a'
assert record.title == 'Title'
class TestFieldUnification:
def test_field_with_data_creates_control_field(self):
field = Field('001', data='12345')
assert field.is_control_field()
assert field.tag == '001'
assert field.data == '12345'
def test_data_field_is_not_control(self):
field = Field('245', '1', '0')
assert not field.is_control_field()
assert field.data is None
def test_control_field_isinstance(self):
cf = ControlField('001', '12345')
assert isinstance(cf, Field)
assert cf.is_control_field()
def test_control_field_backward_compat_class(self):
cf = ControlField('003', 'DLC')
assert cf.tag == '003'
assert cf.data == 'DLC'
assert cf.is_control_field()
def test_record_getitem_returns_field_for_control(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
field = record['001']
assert isinstance(field, Field)
assert field.data == 'test-id'
def test_record_getitem_raises_keyerror(self):
record = Record(Leader())
with pytest.raises(KeyError):
record['999']
def test_record_get_returns_none_for_missing(self):
record = Record(Leader())
assert record.get('999') is None
assert record.get('001') is None
def test_record_get_returns_field_for_existing(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
field = record.get('001')
assert isinstance(field, Field)
assert field.data == 'test-id'
def test_get_fields_includes_control_fields(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
record.add_control_field('003', 'DLC')
record.add_field(Field('245', '1', '0', subfields=[Subfield('a', 'Title')]))
all_fields = record.get_fields()
tags = [f.tag for f in all_fields]
assert '001' in tags
assert '003' in tags
assert '245' in tags
for f in all_fields:
assert isinstance(f, Field)
def test_get_fields_by_control_tag(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
fields = record.get_fields('001')
assert len(fields) == 1
assert fields[0].data == 'test-id'
def test_add_field_with_control_field(self):
record = Record(Leader())
record.add_field(Field('001', data='12345'))
assert record.control_field('001') == '12345'
def test_fields_method_includes_control_fields(self):
record = Record(Leader())
record.add_control_field('001', 'test-id')
record.add_field(Field('245', '1', '0', subfields=[Subfield('a', 'Title')]))
all_fields = record.fields()
tags = [f.tag for f in all_fields]
assert '001' in tags
assert '245' in tags
def test_default_indicators_are_spaces(self):
field = Field('245')
assert field.indicator1 == ' '
assert field.indicator2 == ' '
class TestFieldStringRepresentation:
def test_data_field_str(self):
field = Field('245', '1', '0', subfields=[
Subfield('a', 'The Great Gatsby'),
Subfield('c', 'F. Scott Fitzgerald'),
])
assert str(field) == '=245 10$aThe Great Gatsby$cF. Scott Fitzgerald'
def test_control_field_str(self):
field = Field('001', data='12345')
assert str(field) == '=001 12345'
def test_data_field_str_blank_indicators(self):
field = Field('650', ' ', '0', subfields=[Subfield('a', 'Python')])
assert str(field) == '=650 \\0$aPython'
def test_field_repr(self):
field = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
r = repr(field)
assert '245' in r
def test_control_field_repr(self):
field = Field('001', data='12345')
r = repr(field)
assert '001' in r
class TestRecordPropertyAccessors:
def _make_record(self):
record = Record()
record.add_field(create_field('245', '1', '0', a='Test Title'))
record.add_field(create_field('100', '1', ' ', a='Smith, John'))
record.add_field(create_field('020', ' ', ' ', a='0201616165'))
record.add_field(create_field('022', ' ', ' ', a='0028-0836'))
record.add_field(create_field('260', ' ', ' ', a='Place :', b='Publisher,', c='2023'))
record.add_field(create_field('650', ' ', '0', a='Testing.'))
record.add_field(create_field('852', ' ', ' ', a='Library'))
record.add_field(create_field('500', ' ', ' ', a='A note.'))
record.add_field(create_field('130', ' ', ' ', a='Uniform'))
record.add_field(create_field('086', ' ', ' ', a='Y 1.1/2:'))
record.add_field(create_field('222', ' ', ' ', a='ISSN Title'))
record.add_field(create_field('024', '8', ' ', a='1234-5678'))
record.add_field(create_field('490', '1', ' ', a='Series Name'))
record.add_field(create_field('300', ' ', ' ', a='100 p.'))
record.add_field(create_field('700', '1', ' ', a='Jones, Mary'))
return record
def test_title_is_property(self):
record = self._make_record()
assert record.title == 'Test Title'
assert not callable(record.title) or isinstance(record.title, str)
def test_author_is_property(self):
record = self._make_record()
assert 'Smith' in record.author
def test_isbn_is_property(self):
record = self._make_record()
assert record.isbn == '0201616165'
def test_issn_is_property(self):
record = self._make_record()
assert record.issn == '0028-0836'
def test_subjects_is_property(self):
record = self._make_record()
assert isinstance(record.subjects, list)
assert 'Testing.' in record.subjects
def test_publisher_is_property(self):
record = self._make_record()
assert record.publisher is not None
def test_location_is_property(self):
record = self._make_record()
assert isinstance(record.location, list)
def test_notes_is_property(self):
record = self._make_record()
assert isinstance(record.notes, list)
def test_uniform_title_is_property(self):
record = self._make_record()
assert record.uniform_title is not None
def test_sudoc_is_property(self):
record = self._make_record()
assert record.sudoc is not None
def test_issn_title_is_property(self):
record = self._make_record()
assert record.issn_title is not None
def test_issnl_is_property(self):
record = self._make_record()
result = record.issnl
assert result is None or isinstance(result, str)
def test_pubyear_returns_str(self):
record = self._make_record()
year = record.pubyear
assert year is not None
assert isinstance(year, str)
assert year == '2023'
def test_pubyear_none_returns_none(self):
record = Record()
assert record.pubyear is None
def test_series_is_property(self):
record = self._make_record()
assert record.series is not None
def test_physical_description_is_property(self):
record = self._make_record()
assert record.physical_description is not None
def test_physicaldescription_alias(self):
record = self._make_record()
assert record.physicaldescription == record.physical_description
def test_uniformtitle_alias(self):
record = self._make_record()
assert record.uniformtitle == record.uniform_title
def test_addedentries(self):
record = self._make_record()
entries = record.addedentries
assert isinstance(entries, list)
assert len(entries) >= 1
assert any('Jones' in str(e) for e in entries)
class TestBulkFieldOperations:
def test_add_multiple_fields(self):
record = Record(Leader())
f1 = Field('100', '1', ' ', subfields=[Subfield('a', 'Author')])
f2 = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
f3 = Field('650', ' ', '0', subfields=[Subfield('a', 'Subject')])
record.add_field(f1, f2, f3)
assert record.get_field('100') is not None
assert record.get_field('245') is not None
assert record.get_field('650') is not None
def test_add_field_single_still_works(self):
record = Record(Leader())
f = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
record.add_field(f)
assert record.get_field('245') is not None
def test_remove_field_by_object(self):
record = Record(Leader())
f = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
record.add_field(f)
record.remove_field(f)
assert record.get_field('245') is None
def test_remove_field_multiple(self):
record = Record(Leader())
f1 = Field('100', '1', ' ', subfields=[Subfield('a', 'Author')])
f2 = Field('650', ' ', '0', subfields=[Subfield('a', 'Subject')])
record.add_field(f1, f2)
record.remove_field(f1, f2)
assert record.get_field('100') is None
assert record.get_field('650') is None
def test_remove_field_returns_none(self):
record = Record(Leader())
f = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
record.add_field(f)
result = record.remove_field(f)
assert result is None
def test_remove_fields_by_tags(self):
record = Record(Leader())
record.add_field(Field('650', ' ', '0', subfields=[Subfield('a', 'Subject')]))
record.add_field(Field('700', '1', ' ', subfields=[Subfield('a', 'Author')]))
record.remove_fields('650', '700')
assert record.get_field('650') is None
assert record.get_field('700') is None
def test_remove_fields_returns_none(self):
record = Record(Leader())
record.add_field(Field('650', ' ', '0', subfields=[Subfield('a', 'Subject')]))
result = record.remove_fields('650')
assert result is None
class TestOrderedFieldInsertion:
def test_add_ordered_field(self):
record = Record(fields=[
Field('100', '1', ' ', subfields=[Subfield('a', 'Author')]),
Field('650', ' ', '0', subfields=[Subfield('a', 'Subject')]),
])
f245 = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
record.add_ordered_field(f245)
tags = [f.tag for f in record.get_fields()]
data_tags = [t for t in tags if t >= '010']
assert data_tags == ['100', '245', '650']
def test_add_ordered_field_at_end(self):
record = Record(fields=[
Field('100', '1', ' ', subfields=[Subfield('a', 'Author')]),
])
f650 = Field('650', ' ', '0', subfields=[Subfield('a', 'Subject')])
record.add_ordered_field(f650)
data_tags = [f.tag for f in record.get_fields() if f.tag >= '010']
assert data_tags == ['100', '650']
def test_add_grouped_field(self):
record = Record(fields=[
Field('650', ' ', '0', subfields=[Subfield('a', 'Subject 1')]),
Field('650', ' ', '0', subfields=[Subfield('a', 'Subject 2')]),
Field('700', '1', ' ', subfields=[Subfield('a', 'Author')]),
])
f650 = Field('650', ' ', '0', subfields=[Subfield('a', 'Subject 3')])
record.add_grouped_field(f650)
data_tags = [f.tag for f in record.get_fields() if f.tag >= '010']
assert data_tags == ['650', '650', '650', '700']
def test_add_grouped_field_no_existing(self):
record = Record(fields=[
Field('100', '1', ' ', subfields=[Subfield('a', 'Author')]),
Field('650', ' ', '0', subfields=[Subfield('a', 'Subject')]),
])
f245 = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
record.add_grouped_field(f245)
data_tags = [f.tag for f in record.get_fields() if f.tag >= '010']
assert data_tags == ['100', '245', '650']
class TestFieldLinkage:
def test_linkage_occurrence_num(self):
field = Field('245', '1', '0', subfields=[
Subfield('6', '880-03'),
Subfield('a', 'Title'),
])
assert field.linkage_occurrence_num() == '03'
def test_linkage_occurrence_num_no_subfield_6(self):
field = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
assert field.linkage_occurrence_num() is None
def test_linkage_occurrence_num_no_dash(self):
field = Field('245', '1', '0', subfields=[
Subfield('6', '880'),
Subfield('a', 'Title'),
])
assert field.linkage_occurrence_num() is None
class TestSubfieldPositionalInsert:
def test_add_subfield_default_appends(self):
field = Field('245', '1', '0')
field.add_subfield('a', 'Title')
field.add_subfield('c', 'Author')
subs = field.subfields()
assert subs[0].code == 'a'
assert subs[1].code == 'c'
def test_add_subfield_at_position(self):
field = Field('245', '1', '0')
field.add_subfield('a', 'Title')
field.add_subfield('c', 'Author')
field.add_subfield('b', 'Subtitle', pos=1)
subs = field.subfields()
assert subs[0].code == 'a'
assert subs[1].code == 'b'
assert subs[2].code == 'c'
def test_add_subfield_at_zero(self):
field = Field('245', '1', '0')
field.add_subfield('c', 'Author')
field.add_subfield('a', 'Title', pos=0)
subs = field.subfields()
assert subs[0].code == 'a'
assert subs[1].code == 'c'
class TestFieldValueMethods:
def test_value_data_field(self):
field = Field('245', '1', '0', subfields=[
Subfield('a', 'The Great Gatsby'),
Subfield('c', 'F. Scott Fitzgerald'),
])
assert field.value() == 'The Great Gatsby F. Scott Fitzgerald'
def test_value_control_field(self):
field = Field('001', data='12345')
assert field.value() == '12345'
def test_value_single_subfield(self):
field = Field('020', ' ', ' ', subfields=[Subfield('a', '0201616165')])
assert field.value() == '0201616165'
def test_format_field_data(self):
field = Field('245', '1', '0', subfields=[
Subfield('a', 'The Great Gatsby'),
Subfield('c', 'F. Scott Fitzgerald'),
])
assert field.format_field() == 'The Great Gatsby F. Scott Fitzgerald'
def test_format_field_control(self):
field = Field('001', data='12345')
assert field.format_field() == '12345'
class TestFieldBinarySerialization:
def test_field_as_marc_returns_bytes(self):
field = Field('245', '1', '0', subfields=[Subfield('a', 'Title')])
result = field.as_marc()
assert isinstance(result, bytes)
assert len(result) > 0
def test_control_field_as_marc(self):
field = Field('001', data='12345')
result = field.as_marc()
assert isinstance(result, bytes)
assert b'12345' in result
def test_field_as_marc21_alias(self):
field = Field('245', '1', '0', subfields=[Subfield('a', 'Test')])
assert field.as_marc() == field.as_marc21()
def test_field_as_marc_contains_subfield_data(self):
field = Field('245', '1', '0', subfields=[
Subfield('a', 'Title'),
Subfield('c', 'Author'),
])
result = field.as_marc()
assert b'Title' in result
assert b'Author' in result
class TestConvenienceFunctions:
def test_map_records(self):
from pathlib import Path
test_file = Path(__file__).parent.parent / 'data' / 'simple_book.mrc'
titles = []
from mrrc import map_records
map_records(lambda r: titles.append(r.title), str(test_file))
assert len(titles) > 0
def test_parse_json_to_array(self):
from mrrc import parse_json_to_array
record = Record(fields=[
Field('245', '1', '0', subfields=[Subfield('a', 'Title')]),
])
record.add_control_field('001', 'test-id')
json_str = record.as_json()
json_array = '[' + json_str + ']'
records = parse_json_to_array(json_array)
assert len(records) == 1
assert isinstance(records[0], Record)
class TestMarcConstants:
def test_constants_importable(self):
from mrrc import (
LEADER_LEN, DIRECTORY_ENTRY_LEN,
END_OF_FIELD, END_OF_RECORD, SUBFIELD_INDICATOR,
)
assert LEADER_LEN == 24
assert DIRECTORY_ENTRY_LEN == 12
assert END_OF_FIELD == '\x1e'
assert END_OF_RECORD == '\x1d'
assert SUBFIELD_INDICATOR == '\x1f'
def test_xml_constants(self):
from mrrc import MARC_XML_NS, MARC_XML_SCHEMA
assert 'loc.gov' in MARC_XML_NS
assert 'loc.gov' in MARC_XML_SCHEMA
class TestExceptionHierarchy:
def test_exception_classes_importable(self):
from mrrc import (
MrrcException,
RecordLengthInvalid,
RecordLeaderInvalid,
BaseAddressInvalid,
BaseAddressNotFound,
RecordDirectoryInvalid,
EndOfRecordNotFound,
FieldNotFound,
FatalReaderError,
)
assert issubclass(RecordLengthInvalid, MrrcException)
assert issubclass(RecordLeaderInvalid, MrrcException)
assert issubclass(BaseAddressInvalid, MrrcException)
assert issubclass(BaseAddressNotFound, MrrcException)
assert issubclass(RecordDirectoryInvalid, MrrcException)
assert issubclass(EndOfRecordNotFound, MrrcException)
assert issubclass(FieldNotFound, MrrcException)
assert issubclass(FatalReaderError, MrrcException)
def test_exception_hierarchy(self):
from mrrc import MrrcException, RecordLengthInvalid
try:
raise RecordLengthInvalid("bad length")
except MrrcException as e:
assert "bad length" in str(e)
def test_exceptions_are_exceptions(self):
from mrrc import MrrcException
assert issubclass(MrrcException, Exception)
class TestLegacySubfields:
def test_convert_legacy_subfields(self):
result = Field.convert_legacy_subfields(['a', 'Title', 'b', 'Subtitle'])
assert len(result) == 2
assert result[0].code == 'a'
assert result[0].value == 'Title'
assert result[1].code == 'b'
assert result[1].value == 'Subtitle'
def test_convert_empty_list(self):
result = Field.convert_legacy_subfields([])
assert result == []
class TestParseXmlToArray:
def test_parse_xml_from_string(self):
xml = '''<?xml version="1.0" encoding="UTF-8"?>
<collection xmlns="http://www.loc.gov/MARC21/slim">
<record>
<leader>00000nam a2200000 a 4500</leader>
<controlfield tag="001">test-id</controlfield>
<datafield tag="245" ind1="1" ind2="0">
<subfield code="a">Test Title</subfield>
</datafield>
</record>
</collection>'''
from mrrc import parse_xml_to_array
records = parse_xml_to_array(xml)
assert len(records) >= 1
assert isinstance(records[0], Record)
def test_parse_xml_from_file_object(self):
import io
xml = '''<?xml version="1.0" encoding="UTF-8"?>
<collection xmlns="http://www.loc.gov/MARC21/slim">
<record>
<leader>00000nam a2200000 a 4500</leader>
<controlfield tag="001">test-id</controlfield>
</record>
</collection>'''
from mrrc import parse_xml_to_array
records = parse_xml_to_array(io.StringIO(xml))
assert len(records) >= 1
def test_parse_xml_returns_list(self):
xml = '''<?xml version="1.0" encoding="UTF-8"?>
<collection xmlns="http://www.loc.gov/MARC21/slim">
</collection>'''
from mrrc import parse_xml_to_array
records = parse_xml_to_array(xml)
assert isinstance(records, list)
if __name__ == '__main__':
pytest.main([__file__, '-v'])