Coverage for pybtexnbib/parsers.py: 100.00%

100 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2026-07-31 06:25 +0000

1import re 

2from dataclasses import dataclass 

3from collections import defaultdict 

4from pathlib import Path 

5from pybtex.database.input import BaseParser 

6from pybtex.database import Entry, Person 

7import csv 

8from warnings import warn 

9from pybtexris.parsers import clean_entry_key, get_entry_key 

10 

11 

12data_dir = Path(__file__).parent/"data" 

13 

14@dataclass 

15class NBIBField: 

16 """ A field in a NBIB file. """ 

17 code: str 

18 value: str 

19 

20 

21class NBIBParser(BaseParser): 

22 """ 

23 Parser for NBIB/Medline/PubMed citation files. 

24 

25 For information, see: 

26 https://www.nlm.nih.gov/bsd/policy/cit_format.html 

27 https://www.nlm.nih.gov/bsd/mms/medlineelements.html 

28 """ 

29 default_suffix = '.nbib' 

30 unicode_io = True 

31 

32 def __init__(self, *args, **kwargs): 

33 super().__init__(*args, **kwargs) 

34 self.nbib_type_to_bibtex = {} 

35 

36 with open(data_dir/"types.csv") as f: 

37 reader = csv.reader(f, delimiter=',') 

38 

39 #skip the header 

40 next(reader, None) 

41 

42 for row in reader: 

43 nbib_type = row[0] 

44 bibtex_type = row[1] 

45 self.nbib_type_to_bibtex[nbib_type] = bibtex_type 

46 

47 def parse_stream(self, stream): 

48 text = stream.read() 

49 return self.parse_string(text) 

50 

51 def process_entry(self, entry_text): 

52 # Read file into list and merge multi-line entries 

53 nbib_fields = [] 

54 for line in entry_text.split('\n'): 

55 m = re.match(r"^([A-Z]{2,4})\s*-\s*(.*)$", line.strip()) 

56 if m: 

57 code = m.group(1) 

58 value = m.group(2).strip() 

59 nbib_fields.append( NBIBField(code=code, value=value) ) 

60 elif len(nbib_fields) > 0: 

61 # If the line doesn't match the pattern then append the text to the previous field 

62 nbib_fields[-1].value += line.strip() 

63 else: 

64 warn(f"First line of NBIB file '{line}' is invalid.") 

65 continue 

66 

67 # Parse nbib fields 

68 nbib_dict = defaultdict(list) 

69 for field in nbib_fields: 

70 nbib_dict[field.code].append(field.value) 

71 

72 # Get publication type 

73 nbib_publication_types = nbib_dict.pop("PT", []) 

74 bibtex_types = { 

75 self.nbib_type_to_bibtex[nbib_type] 

76 for nbib_type in nbib_publication_types 

77 if nbib_type in self.nbib_type_to_bibtex 

78 } 

79 bibtex_type = bibtex_types.pop() if len(bibtex_types) == 1 else "misc" 

80 nbib_publication_description = "; ".join(nbib_publication_types) 

81 

82 # Create Entry object 

83 entry = Entry(bibtex_type) 

84 if nbib_publication_description: 

85 entry.fields["type"] = nbib_publication_description 

86 

87 # Read People 

88 def add_person(code, role): 

89 names = nbib_dict.pop(code, []) 

90 for name in names: 

91 person = Person(name) 

92 entry.add_person(person, role) 

93 

94 add_person("FAU", "author") 

95 add_person("FED", "editor") 

96 # what should be done for AU and ED?  

97 

98 # Read Other Fields 

99 def add_field(code, bibtex_field, delimiter="; "): 

100 values = nbib_dict.pop(code, []) 

101 for value in values: 

102 if bibtex_field in entry.fields: 

103 entry.fields[bibtex_field] += f"{delimiter}{value}" 

104 else: 

105 entry.fields[bibtex_field] = value 

106 

107 add_field("TI", "title") 

108 add_field("JT", "journal") 

109 add_field("JTI", "shortjournal") 

110 add_field("DP", "date") 

111 

112 add_field("BTI", "booktitle") 

113 add_field("PB", "publisher") 

114 add_field("CY", "address") 

115 add_field("VI", "volume") 

116 add_field("PG", "pages") 

117 add_field("OT", "keywords", delimiter=" | ") 

118 add_field("GN", "note", delimiter=" | ") 

119 add_field("ISBN", "isbn") 

120 add_field("IS", "issn") 

121 add_field("AB", "abstract") 

122 

123 add_field("AB", "abstract") 

124 

125 # Read year from date field if possible 

126 if "date" in entry.fields: 

127 m = re.match(r"^(\d{4})($|\D)", entry.fields['date']) 

128 if m: 

129 entry.fields['year'] = m.group(1) 

130 

131 # Get DOI 

132 values = nbib_dict.pop("AID", []) 

133 for value in values: 

134 if "[doi]" in value: 

135 entry.fields["doi"] = value.replace("[doi]", "").strip() 

136 else: 

137 nbib_dict["AID"].append(value) 

138 

139 # Add the remaining fields with the code as the field name 

140 for code, values in nbib_dict.items(): 

141 entry.fields[code] = "; ".join(values) 

142 

143 entry_key = get_entry_key(entry) 

144 

145 return entry_key,entry 

146 

147 def parse_string(self, text): 

148 self.unnamed_entry_counter = 1 

149 self.command_start = 0 

150 

151 entry_texts = re.split(r"ER\s+-", text) 

152 entries = (self.process_entry(t) for t in entry_texts if t.strip()) 

153 self.data.add_entries(entries) 

154 return self.data 

155 

156 

157