Source code for mcot.bibtex.entry

"""
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)