"""
Possible entry types from https://en.wikipedia.org/wiki/BibTeX:
- article
- An article from a journal or magazine.
- Required fields: author, title, journal, year, volume
- Optional fields: number, pages, month, note, key
- book
- A book with an explicit publisher.
- Required fields: author/editor, title, publisher, year
- Optional fields: volume/number, series, address, edition, month, note, key
- booklet
- A work that is printed and bound, but without a named publisher or sponsoring institution.
- Required fields: title
- Optional fields: author, howpublished, address, month, year, note, key
- conference
- The same as inproceedings, included for Scribe compatibility.
- inbook
- A part of a book, usually untitled. May be a chapter (or section, etc.) and/or a range of pages.
- Required fields: author/editor, title, chapter/pages, publisher, year
- Optional fields: volume/number, series, type, address, edition, month, note, key
- incollection
- A part of a book having its own title.
- Required fields: author, title, booktitle, publisher, year
- Optional fields: editor, volume/number, series, type, chapter, pages, address, edition, month, note, key
- inproceedings
- An article in a conference proceedings.
- Required fields: author, title, booktitle, year
- Optional fields: editor, volume/number, series, pages, address, month, organization, publisher, note, key
- manual
- Technical documentation.
- Required fields: title
- Optional fields: author, organization, address, edition, month, year, note, key
- mastersthesis
- A Master's thesis.
- Required fields: author, title, school, year
- Optional fields: type, address, month, note, key
- misc
- For use when nothing else fits.
- Required fields: none
- Optional fields: author, title, howpublished, month, year, note, key
- phdthesis
- A Ph.D. thesis.
- Required fields: author, title, school, year
- Optional fields: type, address, month, note, key
- proceedings
- The proceedings of a conference.
- Required fields: title, year
- Optional fields: editor, volume/number, series, address, month, publisher, organization, note, key
- techreport
- A report published by a school or other institution, usually numbered within a series.
- Required fields: author, title, institution, year
- Optional fields: type, number, address, month, note, key
- unpublished
- A document having an author and title, but not formally published.
- Required fields: author, title, note
- Optional fields: month, year, key
"""
from collections import defaultdict
from typing import DefaultDict, Tuple, Union
import traitlets
required_tags: DefaultDict[str, Tuple[Union[Tuple[str, ...], str], ...]] = defaultdict(tuple)
required_tags.update({
'article': ('author', 'title', 'journal', 'year', 'volume'),
'book': (('author', 'editor'), 'title', 'publisher', 'year'),
'booklet': ('title',),
'conference': ('author', 'title', 'booktitle', 'year'),
'inbook': (('author', 'editor'), 'title', ('chapter', 'pages'), 'publisher', 'year'),
'incollection': (('author', 'editor'), 'title', 'booktitle', 'publisher', 'year'),
'inproceedings': ('author', 'title', 'booktitle', 'year'),
'manual': ('title', ),
'masterthesis': ('author', 'title', 'school', 'year'),
'misc': (),
'phdthesis': ('author', 'title', 'school', 'year'),
'proceedings': ('title', 'year'),
'techreport': ('author', 'title', 'institution', 'year'),
'unpublished': ('author', 'title', 'note'),
})
class FieldError(KeyError):
"""Error with a Bibtex field entry"""
[docs]class BibTexEntry(traitlets.HasTraits):
type = traitlets.Unicode()
tags = traitlets.Dict(traitlets.Unicode())
key = traitlets.Unicode()
[docs] def __init__(self, type, key, check=True, **tags):
self.type = type.lower()
self.key = key
self.tags = tags
if check:
self.check_complete()
[docs] def adjust_id(self, other_entries):
if self.key in other_entries.entries:
for add in 'abcdefghijklmnopqrstuvwxyz':
if self.key + add not in other_entries.entries:
self.key += add
break
def __getitem__(self, item):
return self.tags[item]
def __setitem__(self, item, value):
self.tags[item] = value
[docs] def get(self, field_name, default):
return self.tags.get(field_name, default)
[docs] def check_complete(self):
for field in required_tags[self.type]:
if isinstance(field, tuple):
if all(f not in self.tags for f in field):
raise FieldError('%r\n At least one of %s should be supplied' % (self, field))
elif field not in self.tags:
raise FieldError('%r\n Required field %s has not been supplied' % (self, field))
def __str__(self, ):
return '%s: %s' % (self.type, self.key)
def __repr__(self, ):
base = '@%s{%s,' % (self.type, self.key)
spaces = ' ' * (len(self.type) + 2)
sorted_names = sorted(sorted(self.tags.keys(),
key=lambda key: key.lower()),
key=lambda key: key not in required_tags[self.type])
field_str = ['%s%s = {%s},' % (spaces, name, self.tags[name]) for name in sorted_names]
field_str[-1] = field_str[-1][:-1] # remove comma from the last tag
return "\n".join([base] + field_str + ['}'])
def __contains__(self, item):
return item in self.tags
def __eq__(self, other):
if not isinstance(other, BibTexEntry):
return False
if self is other:
return True
for id in ['doi', 'PMID', 'pii', 'eid']:
if len(self.get(id, '')) > 0 and len(other.get(id, '')) > 0:
return self[id] == other[id]
for tag_name in self.tags.keys():
if tag_name in other and self[tag_name] != other[tag_name]:
return False
return True
[docs] def url(self, ):
if 'url' in self:
return self['url']
if 'adsurl' in self:
return self['adsurl']
if 'doi' in self:
doi = self['doi']
if doi.startswith('http'):
return doi
elif doi.startswith('doi:'):
doi = doi[4:]
return "https://doi.org/" + doi
raise ValueError(f"No URL defined for {self}")
[docs] def cite_text(self, ):
authors = self.authors
if len(authors) <= 2:
start = ' and '.join([a.last_name for a in authors])
else:
start = authors[0].last_name + ' et al.'
if 'year' in self:
return f'{start} ({self["year"]})'
else:
return start
@property
def authors(self, ):
return tuple(Author.parse(author.strip()) for author in self.tags.get('author', '').split(' and ') if len(author.strip()) > 0)
[docs]class Author:
[docs] def __init__(self, last_name, first_names: Tuple[str, ...]=()):
self.first_names = tuple(first_names)
self.last_name = last_name
[docs] @classmethod
def parse(cls, name):
if ',' in name:
last, all_first = name.split(',')
first = all_first.split()
else:
*first, last = name.split()
return cls(last, first)
@property
def short_name(self, ):
filtered_first_names = [name for name in self.first_names if not (
len(name) == 1 or
(len(name) == 2 and name[-1] == '.') or
name.isupper()
)]
if len(filtered_first_names) == 0:
raise ValueError(f"No short name available for {self}")
return ' '.join(filtered_first_names) + ' ' + self.last_name
def __str__(self, ):
return self.last_name + ', ' + ' '.join(self.first_names)