import io
import json
from pathlib import Path
import pytest
from mrrc import (
ControlField,
Field,
Indicators,
Leader,
MARCReader,
MARCWriter,
Record,
Subfield,
)
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 (
DIRECTORY_ENTRY_LEN,
END_OF_FIELD,
END_OF_RECORD,
LEADER_LEN,
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 (
BaseAddressInvalid,
BaseAddressNotFound,
EndOfRecordNotFound,
FatalReaderError,
FieldNotFound,
MrrcException,
RecordDirectoryInvalid,
RecordLeaderInvalid,
RecordLengthInvalid,
)
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"])