mrrc 0.7.6

A Rust library for reading, writing, and manipulating MARC bibliographic records in ISO 2709 binary format
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
"""
PyMARC Compliance Tests
========================

These tests are ported directly from the pymarc test suite to ensure
complete API compatibility. The logic is preserved strictly from pymarc
to help identify API gaps and ensure our Python wrapper achieves 100%
compatibility with the original pymarc library.

Reference: https://gitlab.com/pymarc/pymarc/-/tree/main/test
"""

import pytest
from mrrc import MARCReader, Record, Field, Leader
import io


# ============================================================================
# Test Fixtures - Sample data and helpers
# ============================================================================

def create_field(tag, ind1=' ', ind2=' ', **subfields):
    """PYMARC COMPAT: Helper to create a field with subfields."""
    field = Field(tag, ind1, ind2)
    for code, value in subfields.items():
        field.add_subfield(code, value)
    return field


# ============================================================================
# Record Tests (from pymarc test_record.py)
# ============================================================================

class TestRecordAddField:
    """PYMARC COMPAT: test_add_field"""
    
    def test_add_field(self):
        """Test basic field addition to a record."""
        record = Record()
        field = Field('245', '1', '0')
        field.add_subfield('a', 'Python')
        field.add_subfield('c', 'Guido')
        record.add_field(field)
        
        assert field in record.fields()


class TestRecordFields:
    """PYMARC COMPAT: test_fields"""
    
    def test_fields_access(self):
        """Test accessing fields via dictionary-like syntax."""
        record = Record()
        
        field1 = Field('245', '1', '0')
        field1.add_subfield('a', 'Python')
        field1.add_subfield('c', 'Guido')
        record.add_field(field1)
        
        field2 = Field('260', ' ', ' ')
        field2.add_subfield('a', 'Amsterdam')
        record.add_field(field2)
        
        assert record['245'] is not None
        assert record['245']['a'] == 'Python'
        assert record['260']['a'] == 'Amsterdam'


class TestRecordRemoveField:
    """PYMARC COMPAT: test_remove_field"""
    
    def test_remove_existing_field(self):
        """Test removing a field that exists."""
        record = Record()
        field = Field('245', '1', '0')
        field.add_subfield('a', 'Python')
        field.add_subfield('c', 'Guido')
        record.add_field(field)
        
        assert record['245']['a'] == 'Python'
        
        # Remove the field
        record.remove_field(field)
        assert record.get_field('245') is None


class TestRecordQuickAccess:
    """PYMARC COMPAT: test_quick_access"""
    
    def test_quick_access_syntax(self):
        """Test record[tag] quick access."""
        record = Record()
        title = Field('245', '1', '0')
        title.add_subfield('a', 'Python')
        title.add_subfield('c', 'Guido')
        record.add_field(title)
        
        assert record['245'] == title
        assert record.get_field('999') is None


class TestRecordMembership:
    """PYMARC COMPAT: test_membership"""
    
    def test_in_operator(self):
        """Test 'in' operator for tag membership."""
        record = Record()
        title = Field('245', '1', '0')
        title.add_subfield('a', 'Python')
        record.add_field(title)
        
        assert '245' in record
        assert '999' not in record


class TestRecordFind:
    """PYMARC COMPAT: test_find"""
    
    def test_get_fields_by_tag(self):
        """Test get_fields() to retrieve multiple fields by tag."""
        record = Record()
        
        subject1 = Field('650', ' ', '0')
        subject1.add_subfield('a', 'Programming Language')
        record.add_field(subject1)
        
        subject2 = Field('650', ' ', '0')
        subject2.add_subfield('a', 'Object Oriented')
        record.add_field(subject2)
        
        found = record.get_fields('650')
        assert len(found) == 2
        assert found[0].subfields_by_code('a')[0] == 'Programming Language'
        assert found[1].subfields_by_code('a')[0] == 'Object Oriented'
        
        # Test get_fields() with no tag (returns all)
        found_all = record.get_fields()
        assert len(found_all) == 2


class TestRecordMultiFind:
    """PYMARC COMPAT: test_multi_find"""
    
    def test_get_fields_multiple_tags(self):
        """Test get_fields() with multiple tags."""
        record = Record()
        
        subject1 = Field('650', ' ', '0')
        subject1.add_subfield('a', 'Programming Language')
        record.add_field(subject1)
        
        subject2 = Field('651', ' ', '0')
        subject2.add_subfield('a', 'Object Oriented')
        record.add_field(subject2)
        
        found = record.get_fields('650', '651')
        assert len(found) == 2


class TestRecordFieldNotFound:
    """PYMARC COMPAT: test_field_not_found"""
    
    def test_empty_record_fields(self):
        """Test that an empty record has no fields."""
        record = Record()
        assert len(record.fields()) == 0


class TestRecordAuthor:
    """PYMARC COMPAT: test_author"""
    
    def test_author_from_100_field(self):
        """Test getting author from 100 field."""
        record = Record()
        assert record.author is None
        
        record.add_field(create_field('100', '1', ' ', 
                                      a='Bletch, Foobie,',
                                      d='1979-1981.'))
        assert record.author is not None
        assert 'Bletch' in record.author


class TestRecordUniformTitle:
    """PYMARC COMPAT: test_uniformtitle"""
    
    def test_uniform_title_from_130_field(self):
        """Test getting uniform title from 130 field."""
        record = Record()
        assert record.uniform_title is None
        
        record.add_field(create_field('130', '0', ' ',
                                      a='Tosefta.',
                                      l='English.',
                                      f='1977.'))
        uniform_title = record.uniform_title
        assert uniform_title is not None


class TestRecordSubjects:
    """PYMARC COMPAT: test_subjects"""

    def test_subjects_from_650_fields(self):
        """Test getting subjects from 650 fields."""
        record = Record()

        record.add_field(create_field('650', ' ', '0', a='Computer science'))
        record.add_field(create_field('650', ' ', '0', a='Python language'))

        subjects = record.subjects
        assert len(subjects) >= 2
        assert any('Computer' in s for s in subjects)

    def test_subjects_from_all_6xx_fields(self):
        """Test that subjects() returns entries from all 6xx fields, matching pymarc."""
        record = Record()

        record.add_field(create_field('600', '1', '0', a='Maimonides, Moses,'))
        record.add_field(create_field('610', '2', '0', a='United Nations'))
        record.add_field(create_field('611', '2', '0', a='Vatican Council'))
        record.add_field(create_field('630', '0', '4', a='Talmud Bavli.'))
        record.add_field(create_field('648', ' ', '0', a='20th century'))
        record.add_field(create_field('650', ' ', '0', a='Jewish law.'))
        record.add_field(create_field('651', ' ', '0', a='Jerusalem'))
        record.add_field(create_field('655', ' ', '7', a='Commentaries.'))

        subjects = record.subjects
        assert len(subjects) == 8
        assert 'Maimonides, Moses,' in subjects
        assert 'United Nations' in subjects
        assert 'Vatican Council' in subjects
        assert 'Talmud Bavli.' in subjects
        assert '20th century' in subjects
        assert 'Jewish law.' in subjects
        assert 'Jerusalem' in subjects
        assert 'Commentaries.' in subjects


class TestRecordPublisher:
    """PYMARC COMPAT: test_publisher"""
    
    def test_publisher_from_260_field(self):
        """Test getting publisher from 260 field."""
        record = Record()
        assert record.publisher is None
        
        record.add_field(create_field('260', ' ', ' ',
                                      a='Paris :',
                                      b='Gauthier-Villars ;',
                                      c='1955.'))
        assert record.publisher is not None
        assert 'Villars' in record.publisher


    def test_publisher_from_264_rda_field(self):
        """Test getting publisher from 264 field (RDA cataloging)."""
        record = Record()
        record.add_field(create_field('264', ' ', '1',
                                      a='Cambridge, Massachusetts :',
                                      b='The MIT Press,',
                                      c='[2022]'))
        assert record.publisher == 'The MIT Press,'

    def test_publisher_prefers_260_over_264(self):
        """Test that 260 is preferred when both 260 and 264 exist."""
        record = Record()
        record.add_field(create_field('260', ' ', ' ',
                                      b='Old Publisher,'))
        record.add_field(create_field('264', ' ', '1',
                                      b='New Publisher,'))
        assert record.publisher == 'Old Publisher,'

    def test_publisher_ignores_264_non_publication(self):
        """Test that 264 with ind2 != '1' is not used for publisher."""
        record = Record()
        record.add_field(create_field('264', ' ', '3',
                                      b='Some Printer,'))
        assert record.publisher is None


class TestRecordPublicationYear:
    """PYMARC COMPAT: test_pubyear"""

    def test_publication_year_from_260_field(self):
        """Test getting publication year from 260 field."""
        record = Record()
        assert record.pubyear is None

        record.add_field(create_field('260', ' ', ' ',
                                      a='Paris :',
                                      b='Gauthier-Villars ;',
                                      c='1955.'))
        year = record.pubyear
        assert year is not None

    def test_publication_year_from_264_rda_field(self):
        """Test getting publication year from 264 field (RDA cataloging)."""
        record = Record()
        record.add_field(create_field('264', ' ', '1',
                                      c='[2022]'))
        assert record.pubyear == '2022'


class TestRecordISBN:
    """PYMARC COMPAT: test_isbn"""
    
    def test_isbn_from_020_field(self):
        """Test getting ISBN from 020 field."""
        record = Record()
        assert record.isbn is None
        
        record.add_field(create_field('020', ' ', ' ', a='0914378287'))
        assert record.isbn is not None


class TestRecordISSN:
    """PYMARC COMPAT: test_issn"""
    
    def test_issn_from_022_field(self):
        """Test getting ISSN from 022 field."""
        record = Record()
        assert record.issn is None
        
        record.add_field(create_field('022', ' ', ' ', a='0028-0836'))
        issn = record.issn
        assert issn is not None
        assert '0028-0836' in issn


class TestRecordTitle:
    """PYMARC COMPAT: test_title"""
    
    def test_title_from_245_field(self):
        """Test getting title from 245 field."""
        record = Record()
        assert record.title is None
        
        record.add_field(create_field('245', '1', '0', a='Python Programming'))
        title = record.title
        assert title is not None
        assert 'Python' in title


class TestRecordAsMarc:
    """PYMARC COMPAT: test_as_marc"""
    
    def test_as_marc_serialization(self):
        """Test as_marc() serialization (now to_marc21())."""
        record = Record()
        
        record.add_field(create_field('245', '0', '1', a='The pragmatic programmer'))
        
        # Get binary MARC format
        marc_bytes = record.to_marc21()
        
        assert isinstance(marc_bytes, bytes)
        assert len(marc_bytes) > 0
        
        # First 24 bytes are leader
        assert len(marc_bytes) >= 24


class TestRecordAsJsonXml:
    """PYMARC COMPAT: test_as_json, test_as_xml"""
    
    def test_as_json_format(self):
        """Test as_json() serialization."""
        record = Record()
        record.add_field(create_field('245', '1', '0', a='Title'))
        
        json_str = record.to_json()
        assert json_str is not None
        assert isinstance(json_str, str)


    def test_as_xml_format(self):
        """Test as_xml() serialization."""
        record = Record()
        record.add_field(create_field('245', '1', '0', a='Title'))
        
        xml_str = record.to_xml()
        assert xml_str is not None
        assert isinstance(xml_str, str)
        assert '<' in xml_str


class TestRecordPhysicalDescription:
    """PYMARC COMPAT: test_physicaldescription"""
    
    def test_physical_description(self):
        """Test physical description property."""
        record = Record()
        assert record.physical_description is None
        
        record.add_field(create_field('300', ' ', ' ',
                                      a='1 photographic print :',
                                      b='gelatin silver ;',
                                      c='10 x 56 in.'))
        desc = record.physical_description
        assert desc is not None


class TestRecordLocation:
    """PYMARC COMPAT: test_location"""
    
    def test_location_field(self):
        """Test location fields (852)."""
        record = Record()
        assert record.location == []
        
        record.add_field(create_field('852', ' ', ' ',
                                      a='Main Library',
                                      b='Reference'))
        locs = record.location
        assert len(locs) >= 1


class TestRecordNotes:
    """PYMARC COMPAT: test_notes"""
    
    def test_notes_field(self):
        """Test notes from 5xx fields."""
        record = Record()
        assert record.notes == []
        
        record.add_field(create_field('500', ' ', ' ',
                                      a='This is a general note.'))
        notes = record.notes
        assert len(notes) >= 1


# ============================================================================
# Reader Tests (from pymarc test_reader.py)
# ============================================================================

class TestMARCReaderBasic:
    """PYMARC COMPAT: Basic MARCReader functionality"""
    
    def test_reader_iteration(self, fixture_1k):
        """Test iterating over MARC records."""
        data = io.BytesIO(fixture_1k)
        reader = MARCReader(data)
        
        count = 0
        for record in reader:
            if record:
                count += 1
            if count >= 5:
                break
        
        assert count > 0


class TestMARCReaderRecordCount:
    """PYMARC COMPAT: Count records in file"""
    
    def test_reader_count(self, fixture_1k):
        """Test counting all records."""
        data = io.BytesIO(fixture_1k)
        reader = MARCReader(data)
        
        count = 0
        for record in reader:
            if record:
                count += 1
        
        assert count > 0


# ============================================================================
# Field Tests (from pymarc test_field.py)
# ============================================================================

class TestFieldCreation:
    """PYMARC COMPAT: test_field.py - Field creation"""
    
    def test_field_with_subfields(self):
        """Test creating a field with subfields."""
        field = Field('245', '1', '0')
        field.add_subfield('a', 'The pragmatic programmer :')
        field.add_subfield('b', 'from journeyman to master /')
        
        assert field.tag == '245'
        assert len(field.subfields()) == 2


    def test_field_indicators(self):
        """Test field indicators."""
        field = Field('245', '1', '0')
        assert field.indicator1 == '1'
        assert field.indicator2 == '0'


class TestFieldSubfieldAccess:
    """PYMARC COMPAT: test_field.py - Subfield access"""
    
    def test_subfield_by_code(self):
        """Test getting subfield values by code."""
        field = Field('245', '1', '0')
        field.add_subfield('a', 'Main title')
        field.add_subfield('b', 'Subtitle')
        
        a_values = field.subfields_by_code('a')
        assert len(a_values) == 1
        assert a_values[0] == 'Main title'


    def test_get_subfields_multiple_codes(self):
        """Test getting multiple subfield codes at once."""
        field = Field('260', ' ', ' ')
        field.add_subfield('a', 'New York')
        field.add_subfield('b', 'Publisher')
        field.add_subfield('c', '2023')
        
        values = field.get_subfields('a', 'b')
        assert 'New York' in values
        assert 'Publisher' in values


# ============================================================================
# Leader Tests (from pymarc test_leader.py)
# ============================================================================

class TestLeaderBasics:
    """PYMARC COMPAT: test_leader.py - Leader basics"""
    
    def test_leader_creation(self):
        """Test creating a leader."""
        leader = Leader()
        assert leader.record_type == 'a'
        assert leader.bibliographic_level == 'm'
        assert leader.record_status == 'n'


    def test_leader_properties(self):
        """Test setting leader properties."""
        leader = Leader()
        
        leader.record_type = 'c'
        assert leader.record_type == 'c'
        
        leader.bibliographic_level = 'd'
        assert leader.bibliographic_level == 'd'


# ============================================================================
# Round-Trip and Integration Tests
# ============================================================================

class TestRoundTripSerialization:
    """PYMARC COMPAT: Round-trip serialization tests"""
    
    def test_record_roundtrip(self):
        """Test creating, serializing, and deserializing a record."""
        # Create original
        original = Record()
        original.add_control_field('001', 'original-id-001')
        original.add_field(create_field('245', '1', '0', a='Test Title'))
        original.add_field(create_field('100', '1', ' ', a='Test Author'))
        
        # Serialize
        marc_bytes = original.to_marc21()
        
        # Deserialize
        reader = MARCReader(io.BytesIO(marc_bytes))
        restored = reader.read_record()
        
        assert restored is not None
        assert restored.title is not None
        assert restored.author is not None


class TestMultipleFormats:
    """PYMARC COMPAT: Test output formats"""
    
    def test_json_roundtrip_concept(self):
        """Test JSON serialization."""
        record = Record()
        record.add_field(create_field('245', '1', '0', a='Title'))
        
        json_str = record.to_json()
        assert isinstance(json_str, str)
        assert len(json_str) > 0


    def test_xml_serialization(self):
        """Test XML serialization."""
        record = Record()
        record.add_field(create_field('245', '1', '0', a='Title'))
        
        xml_str = record.to_xml()
        assert isinstance(xml_str, str)
        assert len(xml_str) > 0


if __name__ == '__main__':
    pytest.main([__file__, '-v'])