<commit_before>"""Parser and representation for BibTeX .bib databases.

This parser is derived directly from the WEB source code for BibTeX --
especially section "Reading the database file(s)" -- and hence
(barring bugs in translation) should be fully compatible with BibTeX's
own parser.
"""

__all__ = 'Parser Entry FieldError'.split()

import sys
import re
import collections
import textwrap

from . import messages

# Match sequences of legal identifier characters, except that the
# first is not allowed to be a digit (see id_class)
ID_RE = re.compile('(?![0-9])(?:(?![ \t"#%\'(),={}])[\x20-\x7f])+')
# BibTeX only considers space, tab, and newline to be white space (see
# lex_class)
SPACE_RE = re.compile('[ \t\n]*')

class ParseError(Exception):
    pass

class Parser:
    """A parser for .bib BibTeX database files."""

    def __init__(self, *, month_style='full'):
        """Initialize an empty database.

        This also initializes standard month macros (which are usually
        provided by the style file).  month_style may be 'full' to get
        full names, 'abbrv' to get abbrv.bst-style abbreviated names,
        or None to not initialize month macros.

        The database should be populated by calling parse one or more
        times.  The final contents of the database can be retrieved by
        calling finalize.
        """

        self.__log, self.__errors = [], False
        self.__entries = collections.OrderedDict()

        if month_style == 'full':
            self.__macros = {'jan': 'January',   'feb': 'February',
                             'mar': 'March',     'apr': 'April',
                             'may': 'May',       'jun': 'June',
                             'jul': 'July',      'aug': 'August',
                             'sep': 'September', 'oct': 'October',
                             'nov': 'November',  'dec': 'December'}
        elif month_style == 'abbrv':
            self.__macros = {'jan': 'Jan.',  'feb': 'Feb.',
                             'mar': 'Mar.',  'apr': 'Apr.',
                             'may': 'May',   'jun': 'June',
                             'jul': 'July',  'aug': 'Aug.',
                             'sep': 'Sept.', 'oct': 'Oct.',
                             'nov': 'Nov.',  'dec': 'Dec.'}
        elif month_style == None:
            self.__macros = {}
        else:
            raise ValueError('Unknown month style {}'.format(month_style))

    def string(self, name, value):
        """Declare a macro, just like an @string command."""
        self.__macros[name] = value

    def parse(self, str_or_fp, name=None, *, log_fp=None):
        """Parse the contents of str_or_fp and return self.

        str_or_fp must be a string or a file-like object.  If name is
        not None, it is used as the file name.  Otherwise, the name is
        '<string>' for str objects or str_or_fp.name otherwise.

        If log_fp is not None, it must be a file-local object to which
        warnings and InputErrors will be logged.  This logger will be
        attached to all Pos instances created from the file being
        parsed, so any warnings or InputErrors raised from later
        operations on derived objects (like entries or field values)
        will also be logged to log_fp.

        If there are any errors in the input, raises a (potentially
        bundled) InputError.

        Parse can be called multiple times to parse subsequent .bib
        files.  Later files will have access to, for example, strings
        defined in earlier files.
        """

        if isinstance(str_or_fp, str):
            self.__data = str_or_fp
            fname = name or '<string>'
        else:
            self.__data = str_or_fp.read()
            fname = name or str_or_fp.name
        self.__pos = 0

        # Remove trailing whitespace from lines in data (see input_ln
        # in bibtex.web)
        self.__data = re.sub('[ \t]+$', '', self.__data, flags=re.MULTILINE)
        self.__pos_factory = messages.PosFactory(fname, self.__data, log_fp)

        # Parse entries
        recoverer = messages.InputErrorRecoverer()
        while self.__pos < len(self.__data):
            # Just continue to the next entry if there's an error
            with recoverer:
                self._scan_command_or_entry()
        recoverer.reraise()
        return self

    def finalize(self):
        """Perform final checks and return the database.

        This checks cross-ref validity and returns the database.  The
        database is an ordered dictionary mapping from lower-cased
        keys to Entry objects.

        If there are errors during finalization, raises a (potentially
        bundled) InputError.
        """

        # Check cross-refs (XXX crossrefs must come later)
        recoverer = messages.InputErrorRecoverer()
        for ent in self.__entries.values():
            crossref = ent.get('crossref')
            if crossref is not None and crossref.lower() not in self.__entries:
                with recoverer:
                    ent.pos.raise_error(
                        'unknown crossref `{}\''.format(crossref))
        recoverer.reraise()

        return self.__entries

    def _fail(self, msg, pos=None):
        if pos is None:
            pos = self.__pos
        self.__pos_factory.offset_to_pos(pos).raise_error(msg)

    def _warn(self, msg, pos=None):
        if pos is None:
            pos = self.__pos
        self.__pos_factory.offset_to_pos(pos).warn(msg)

    # Base parsers.  These are the only methods that directly
    # manipulate self.__data.

    def _try_tok(self, regexp, skip_space=True):
        """Scan regexp followed by white space.

        Returns the matched text, or None if the match failed."""
        if isinstance(regexp, str):
            regexp = re.compile(regexp)
        m = regexp.match(self.__data, self.__pos)
        if m is None:
            return None
        self.__pos = m.end()
        if skip_space:
            self._skip_space()
        return m.group(0)

    def _scan_balanced_text(self, term):
        """Scan brace-balanced text terminated with character term."""
        start, level = self.__pos, 0
        while self.__pos < len(self.__data):
            char = self.__data[self.__pos]
            if level == 0 and char == term:
                text = self.__data[start:self.__pos]
                self.__pos += 1
                self._skip_space()
                return text
            elif char == '{':
                level += 1
            elif char == '}':
                level -= 1
                if level < 0:
                    self._fail('unexpected }')
            self.__pos += 1
        self._fail('unterminated string')

    def _skip_space(self):
        # This is equivalent to eat_bib_white_space, except that we do
        # it automatically after every token, whereas bibtex carefully
        # and explicitly does it between every token.
        self.__pos = SPACE_RE.match(self.__data, self.__pos).end()

    # Helpers

    def _tok(self, regexp, fail=None):
        """Scan token regexp or fail with the given message."""
        res = self._try_tok(regexp)
        if res is None:
            assert fail
            self._fail(fail)
        return res

    # Productions

    def _scan_identifier(self):
        return self._tok(ID_RE, 'expected identifier')

    def _scan_command_or_entry(self):
        # See get_bib_command_or_entry_and_process

        # Skip to the next database entry or command
        self._tok('[^@]*')
        pos = self.__pos_factory.offset_to_pos(self.__pos)
        if not self._try_tok('@'):
            return None

        # Scan command or entry type
        typ = self._scan_identifier().lower()

        if typ == 'comment':
            # Believe it or not, BibTeX doesn't do anything with what
            # comes after an @comment, treating it like any other
            # inter-entry noise.
            return None

        left = self._tok('[{(]', 'expected { or ( after entry type')
        right, right_re = (')', '\\)') if left == '(' else ('}', '}')

        if typ == 'preamble':
            # Parse the preamble, but ignore it
            self._scan_field_value()
            self._tok(right_re, 'expected '+right)
            return None

        if typ == 'string':
            name = self._scan_identifier().lower()
            if name in self.__macros:
                self._warn('macro `{}\' redefined'.format(name))
            self._tok('=', 'expected = after string name')
            value = self._scan_field_value()
            self._tok(right_re, 'expected '+right)
            self.__macros[name] = value
            return None

        # Not a command, must be a database entry

        # Scan the entry's database key
        if left == '(':
            # The database key is anything up to a comma, white
            # space, or end-of-line (yes, the key can be empty,
            # and it can include a close paren)
            key = self._tok('[^, \t\n]*')
        else:
            # The database key is anything up to comma, white
            # space, right brace, or end-of-line
            key = self._tok('[^, \t}\n]*')

        # Scan entries (starting with comma or close after key)
        fields = []
        while True:
            if self._try_tok(right_re):
                break
            self._tok(',', 'expected {} or ,'.format(right))
            if self._try_tok(right_re):
                break

            # Scan field name and value
            field = self._scan_identifier().lower()
            self._tok('=', 'expected = after field name')
            value = self._scan_field_value()

            fields.append((field, value))

        if key.lower() in self.__entries:
            self._fail('repeated entry')
        self.__entries[key.lower()] = Entry(fields, typ, key, pos)

    def _scan_field_value(self):
        # See scan_and_store_the_field_value_and_eat_white
        value = self._scan_field_piece()
        while self._try_tok('#'):
            value += self._scan_field_piece()
        # Compress spaces in the text.  Bibtex does this
        # (painstakingly) as it goes, but the final effect is the same
        # (see check_for_and_compress_bib_white_space).
        value = re.sub('[ \t\n]+', ' ', value)
        # Strip leading and trailing space (literally just space, see
        # @<Store the field value string@>)
        return value.strip(' ')

    def _scan_field_piece(self):
        # See scan_a_field_token_and_eat_white
        piece = self._try_tok('[0-9]+')
        if piece is not None:
            return piece
        if self._try_tok('{', skip_space=False):
            return self._scan_balanced_text('}')
        if self._try_tok('"', skip_space=False):
            return self._scan_balanced_text('"')
        opos = self.__pos
        piece = self._try_tok(ID_RE)
        if piece is not None:
            if piece.lower() not in self.__macros:
                self._warn('unknown macro `{}\''.format(piece), opos)
                return ''
            return self.__macros[piece.lower()]
        self._fail('expected string, number, or macro name')

class FieldError(KeyError):
    def __init__(self, field, entry=None):
        super().__init__(field)
        self.__entry = entry

    def __str__(self):
        return '{}: missing field `{}\''.format(self.__entry, self.args[0])

MONTH_MACROS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split()

MONTHS = 'January February March April May June July August September October November December'.lower().split()

class Entry(collections.OrderedDict):
    """An entry in a BibTeX database.

    This is an ordered dictionary of fields, plus three additional
    properties: typ gives the type of the entry, such as "journal",
    canonicalized to lower case; key gives the database entry key
    (case is preserved, but should be ignored for comparisons); and
    pos is a messages.Pos instance giving the position of this entry
    in the database file.

    Field values are as they would be seen by a .bst file: white space
    is cleaned up, but they retain macros, BibTeX-style accents, etc.
    Use algo.tex_to_unicode to interpret field values to user-friendly
    Unicode strings.
    """

    def __init__(self, fields, typ=None, key=None, pos=None):
        super().__init__(fields)
        self.typ, self.key, self.pos = typ, key, pos

    def copy(self):
        return self.__class__(self, self.typ, self.key, self.pos)

    def __str__(self):
        return '`{}\' at {}'.format(self.key, self.pos)

    def __getitem__(self, field):
        try:
            return super().__getitem__(field)
        except KeyError:
            raise FieldError(field, self)

    def __eq__(self, o):
        """Two Entries are equal if they have the same fields, type, and key."""
        return super().__eq__(o) and self.typ == o.typ and self.key == o.key

    def to_bib(self, *, month_to_macro=True, wrap_width=70):
        """Return this entry formatted as a BibTeX .bib entry.

        If month_to_macro is True, attempt to parse month names and
        replace them with their standard macro.

        If wrap_width is not None, word wrap the entry at this many
        columns (long words and hyphens are not split).
        """

        lines = ['@%s{%s,' % (self.typ, self.key)]
        for k, v in self.fields.items():
            if month_to_macro and k == 'month':
                try:
                    macro = MONTH_MACROS[1 + self.month_num()]
                    lines.append('  {:12} = {},'.format(k, macro))
                    continue
                except ValueError:
                    pass

            start = '  {:12} = {{'.format(k)
            if wrap_width is not None:
                lines.append(textwrap.fill(
                    v, width=wrap_width,
                    # Keep whitespace formatting as it is
                    expand_tabs=False, replace_whitespace=False,
                    # Don't break long things like URLs
                    break_long_words=False, break_on_hyphens=False,
                    initial_indent=start, subsequent_indent='    ') + '},')
            else:
                lines.append(start + v + '},')
        lines.append('}')
        return '\n'.join(lines)

    def resolve_crossref(self, entries):
        """Return a new entry with crossref-ed fields incorporated.

        entries must be the database in which to find any crossref-ed
        database entries.
        """
        if 'crossref' not in self:
            return self
        nentry = self.copy()
        for k, v in entries[self['crossref'].lower()].items():
            nentry.setdefault(k, v)
        del nentry['crossref']
        return nentry

    def date_key(self):
        """Return a sort key appropriate for sorting by date.

        Returns a tuple ([year, [month]]) where year and month are
        numeric.  Raises InputError if the entry has year and/or month
        fields, but they are malformed.
        """

        key = ()
        year, month = self.get('year'), self.get('month')
        if year is not None:
            if not year.isdigit():
                # XXX Use field position
                self.pos.raise_error('invalid year `{}\''.format(year))
            key += (int(year),)
        if month is not None:
            if year is None:
                # XXX Use field position
                self.pos.raise_error('month without year')
            key += (self.month_num(),)
        return key

    def authors(self, field='author'):
        """Return a list of parsed author names."""
        from .algo import parse_names
        return parse_names(self[field])

    def month_num(self, field='month'):
        """Convert the month of this entry into a number in [1,12].

        This performs fairly fuzzy parsing that supports all standard
        month macro styles (and then some).

        Raises KeyError if this entry does not have the specified
        field and InputError if the field cannot be parsed.
        """
        val = self[field].strip().rstrip('.').lower()
        for i, name in enumerate(MONTHS):
            if name.startswith(val) and len(val) >= 3:
                return i + 1
        # XXX Use field position
        self.pos.raise_error('invalid month `{}\''.format(self[field]))
<commit_msg>Track field positions for more precise field errors<commit_after>"""Parser and representation for BibTeX .bib databases.

This parser is derived directly from the WEB source code for BibTeX --
especially section "Reading the database file(s)" -- and hence
(barring bugs in translation) should be fully compatible with BibTeX's
own parser.
"""

__all__ = 'Parser Entry FieldError'.split()

import sys
import re
import collections
import textwrap

from . import messages

# Match sequences of legal identifier characters, except that the
# first is not allowed to be a digit (see id_class)
ID_RE = re.compile('(?![0-9])(?:(?![ \t"#%\'(),={}])[\x20-\x7f])+')
# BibTeX only considers space, tab, and newline to be white space (see
# lex_class)
SPACE_RE = re.compile('[ \t\n]*')

class ParseError(Exception):
    pass

class Parser:
    """A parser for .bib BibTeX database files."""

    def __init__(self, *, month_style='full'):
        """Initialize an empty database.

        This also initializes standard month macros (which are usually
        provided by the style file).  month_style may be 'full' to get
        full names, 'abbrv' to get abbrv.bst-style abbreviated names,
        or None to not initialize month macros.

        The database should be populated by calling parse one or more
        times.  The final contents of the database can be retrieved by
        calling finalize.
        """

        self.__log, self.__errors = [], False
        self.__entries = collections.OrderedDict()

        if month_style == 'full':
            self.__macros = {'jan': 'January',   'feb': 'February',
                             'mar': 'March',     'apr': 'April',
                             'may': 'May',       'jun': 'June',
                             'jul': 'July',      'aug': 'August',
                             'sep': 'September', 'oct': 'October',
                             'nov': 'November',  'dec': 'December'}
        elif month_style == 'abbrv':
            self.__macros = {'jan': 'Jan.',  'feb': 'Feb.',
                             'mar': 'Mar.',  'apr': 'Apr.',
                             'may': 'May',   'jun': 'June',
                             'jul': 'July',  'aug': 'Aug.',
                             'sep': 'Sept.', 'oct': 'Oct.',
                             'nov': 'Nov.',  'dec': 'Dec.'}
        elif month_style == None:
            self.__macros = {}
        else:
            raise ValueError('Unknown month style {}'.format(month_style))

    def string(self, name, value):
        """Declare a macro, just like an @string command."""
        self.__macros[name] = value

    def parse(self, str_or_fp, name=None, *, log_fp=None):
        """Parse the contents of str_or_fp and return self.

        str_or_fp must be a string or a file-like object.  If name is
        not None, it is used as the file name.  Otherwise, the name is
        '<string>' for str objects or str_or_fp.name otherwise.

        If log_fp is not None, it must be a file-local object to which
        warnings and InputErrors will be logged.  This logger will be
        attached to all Pos instances created from the file being
        parsed, so any warnings or InputErrors raised from later
        operations on derived objects (like entries or field values)
        will also be logged to log_fp.

        If there are any errors in the input, raises a (potentially
        bundled) InputError.

        Parse can be called multiple times to parse subsequent .bib
        files.  Later files will have access to, for example, strings
        defined in earlier files.
        """

        if isinstance(str_or_fp, str):
            self.__data = str_or_fp
            fname = name or '<string>'
        else:
            self.__data = str_or_fp.read()
            fname = name or str_or_fp.name
        self.__pos = 0

        # Remove trailing whitespace from lines in data (see input_ln
        # in bibtex.web)
        self.__data = re.sub('[ \t]+$', '', self.__data, flags=re.MULTILINE)
        self.__pos_factory = messages.PosFactory(fname, self.__data, log_fp)

        # Parse entries
        recoverer = messages.InputErrorRecoverer()
        while self.__pos < len(self.__data):
            # Just continue to the next entry if there's an error
            with recoverer:
                self._scan_command_or_entry()
        recoverer.reraise()
        return self

    def finalize(self):
        """Perform final checks and return the database.

        This checks cross-ref validity and returns the database.  The
        database is an ordered dictionary mapping from lower-cased
        keys to Entry objects.

        If there are errors during finalization, raises a (potentially
        bundled) InputError.
        """

        # Check cross-refs (XXX crossrefs must come later)
        recoverer = messages.InputErrorRecoverer()
        for ent in self.__entries.values():
            crossref = ent.get('crossref')
            if crossref is not None and crossref.lower() not in self.__entries:
                with recoverer:
                    ent.pos.raise_error(
                        'unknown crossref `{}\''.format(crossref))
        recoverer.reraise()

        return self.__entries

    def _fail(self, msg, pos=None):
        if pos is None:
            pos = self.__pos
        self.__pos_factory.offset_to_pos(pos).raise_error(msg)

    def _warn(self, msg, pos=None):
        if pos is None:
            pos = self.__pos
        self.__pos_factory.offset_to_pos(pos).warn(msg)

    # Base parsers.  These are the only methods that directly
    # manipulate self.__data.

    def _try_tok(self, regexp, skip_space=True):
        """Scan regexp followed by white space.

        Returns the matched text, or None if the match failed."""
        if isinstance(regexp, str):
            regexp = re.compile(regexp)
        m = regexp.match(self.__data, self.__pos)
        if m is None:
            return None
        self.__pos = m.end()
        if skip_space:
            self._skip_space()
        return m.group(0)

    def _scan_balanced_text(self, term):
        """Scan brace-balanced text terminated with character term."""
        start, level = self.__pos, 0
        while self.__pos < len(self.__data):
            char = self.__data[self.__pos]
            if level == 0 and char == term:
                text = self.__data[start:self.__pos]
                self.__pos += 1
                self._skip_space()
                return text
            elif char == '{':
                level += 1
            elif char == '}':
                level -= 1
                if level < 0:
                    self._fail('unexpected }')
            self.__pos += 1
        self._fail('unterminated string')

    def _skip_space(self):
        # This is equivalent to eat_bib_white_space, except that we do
        # it automatically after every token, whereas bibtex carefully
        # and explicitly does it between every token.
        self.__pos = SPACE_RE.match(self.__data, self.__pos).end()

    # Helpers

    def _tok(self, regexp, fail=None):
        """Scan token regexp or fail with the given message."""
        res = self._try_tok(regexp)
        if res is None:
            assert fail
            self._fail(fail)
        return res

    # Productions

    def _scan_identifier(self):
        return self._tok(ID_RE, 'expected identifier')

    def _scan_command_or_entry(self):
        # See get_bib_command_or_entry_and_process

        # Skip to the next database entry or command
        self._tok('[^@]*')
        pos = self.__pos_factory.offset_to_pos(self.__pos)
        if not self._try_tok('@'):
            return None

        # Scan command or entry type
        typ = self._scan_identifier().lower()

        if typ == 'comment':
            # Believe it or not, BibTeX doesn't do anything with what
            # comes after an @comment, treating it like any other
            # inter-entry noise.
            return None

        left = self._tok('[{(]', 'expected { or ( after entry type')
        right, right_re = (')', '\\)') if left == '(' else ('}', '}')

        if typ == 'preamble':
            # Parse the preamble, but ignore it
            self._scan_field_value()
            self._tok(right_re, 'expected '+right)
            return None

        if typ == 'string':
            name = self._scan_identifier().lower()
            if name in self.__macros:
                self._warn('macro `{}\' redefined'.format(name))
            self._tok('=', 'expected = after string name')
            value = self._scan_field_value()
            self._tok(right_re, 'expected '+right)
            self.__macros[name] = value
            return None

        # Not a command, must be a database entry

        # Scan the entry's database key
        if left == '(':
            # The database key is anything up to a comma, white
            # space, or end-of-line (yes, the key can be empty,
            # and it can include a close paren)
            key = self._tok('[^, \t\n]*')
        else:
            # The database key is anything up to comma, white
            # space, right brace, or end-of-line
            key = self._tok('[^, \t}\n]*')

        # Scan entries (starting with comma or close after key)
        fields = []
        field_pos = {}
        while True:
            if self._try_tok(right_re):
                break
            self._tok(',', 'expected {} or ,'.format(right))
            if self._try_tok(right_re):
                break

            # Scan field name and value
            field_off = self.__pos
            field = self._scan_identifier().lower()
            self._tok('=', 'expected = after field name')
            value = self._scan_field_value()

            fields.append((field, value))
            field_pos[field] = self.__pos_factory.offset_to_pos(field_off)

        if key.lower() in self.__entries:
            self._fail('repeated entry')
        self.__entries[key.lower()] = Entry(fields, typ, key, pos, field_pos)

    def _scan_field_value(self):
        # See scan_and_store_the_field_value_and_eat_white
        value = self._scan_field_piece()
        while self._try_tok('#'):
            value += self._scan_field_piece()
        # Compress spaces in the text.  Bibtex does this
        # (painstakingly) as it goes, but the final effect is the same
        # (see check_for_and_compress_bib_white_space).
        value = re.sub('[ \t\n]+', ' ', value)
        # Strip leading and trailing space (literally just space, see
        # @<Store the field value string@>)
        return value.strip(' ')

    def _scan_field_piece(self):
        # See scan_a_field_token_and_eat_white
        piece = self._try_tok('[0-9]+')
        if piece is not None:
            return piece
        if self._try_tok('{', skip_space=False):
            return self._scan_balanced_text('}')
        if self._try_tok('"', skip_space=False):
            return self._scan_balanced_text('"')
        opos = self.__pos
        piece = self._try_tok(ID_RE)
        if piece is not None:
            if piece.lower() not in self.__macros:
                self._warn('unknown macro `{}\''.format(piece), opos)
                return ''
            return self.__macros[piece.lower()]
        self._fail('expected string, number, or macro name')

class FieldError(KeyError):
    def __init__(self, field, entry=None):
        super().__init__(field)
        self.__entry = entry

    def __str__(self):
        return '{}: missing field `{}\''.format(self.__entry, self.args[0])

MONTH_MACROS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split()

MONTHS = 'January February March April May June July August September October November December'.lower().split()

class Entry(collections.OrderedDict):
    """An entry in a BibTeX database.

    This is an ordered dictionary of fields, plus some additional
    properties: typ gives the type of the entry, such as "journal",
    canonicalized to lower case.  key gives the database entry key
    (case is preserved, but should be ignored for comparisons).  pos
    is a messages.Pos instance giving the position of this entry in
    the database file.  field_pos is a simple dictionary from field
    names to message.Pos instances.

    Field values are as they would be seen by a .bst file: white space
    is cleaned up, but they retain macros, BibTeX-style accents, etc.
    Use algo.tex_to_unicode to interpret field values to user-friendly
    Unicode strings.
    """

    def __init__(self, fields, typ=None, key=None, pos=None, field_pos=None):
        super().__init__(fields)
        self.typ, self.key, self.pos, self.field_pos = typ, key, pos, field_pos

    def copy(self):
        return self.__class__(self, self.typ, self.key, self.pos, self.field_pos)

    def __str__(self):
        return '`{}\' at {}'.format(self.key, self.pos)

    def __getitem__(self, field):
        try:
            return super().__getitem__(field)
        except KeyError:
            raise FieldError(field, self)

    def __eq__(self, o):
        """Two Entries are equal if they have the same fields, type, and key."""
        return super().__eq__(o) and self.typ == o.typ and self.key == o.key

    def to_bib(self, *, month_to_macro=True, wrap_width=70):
        """Return this entry formatted as a BibTeX .bib entry.

        If month_to_macro is True, attempt to parse month names and
        replace them with their standard macro.

        If wrap_width is not None, word wrap the entry at this many
        columns (long words and hyphens are not split).
        """

        lines = ['@%s{%s,' % (self.typ, self.key)]
        for k, v in self.fields.items():
            if month_to_macro and k == 'month':
                try:
                    macro = MONTH_MACROS[1 + self.month_num()]
                    lines.append('  {:12} = {},'.format(k, macro))
                    continue
                except ValueError:
                    pass

            start = '  {:12} = {{'.format(k)
            if wrap_width is not None:
                lines.append(textwrap.fill(
                    v, width=wrap_width,
                    # Keep whitespace formatting as it is
                    expand_tabs=False, replace_whitespace=False,
                    # Don't break long things like URLs
                    break_long_words=False, break_on_hyphens=False,
                    initial_indent=start, subsequent_indent='    ') + '},')
            else:
                lines.append(start + v + '},')
        lines.append('}')
        return '\n'.join(lines)

    def resolve_crossref(self, entries):
        """Return a new entry with crossref-ed fields incorporated.

        entries must be the database in which to find any crossref-ed
        database entries.
        """
        if 'crossref' not in self:
            return self
        nentry = self.copy()
        source = entries[self['crossref'].lower()]
        for k, v in source.items():
            if k not in nentry:
                nentry[k] = v
                nentry.field_pos[k] = source.field_pos[k]
        del nentry['crossref']
        return nentry

    def date_key(self):
        """Return a sort key appropriate for sorting by date.

        Returns a tuple ([year, [month]]) where year and month are
        numeric.  Raises InputError if the entry has year and/or month
        fields, but they are malformed.
        """

        key = ()
        year, month = self.get('year'), self.get('month')
        if year is not None:
            if not year.isdigit():
                self.field_pos['year'].raise_error(
                    'invalid year `{}\''.format(year))
            key += (int(year),)
        if month is not None:
            if year is None:
                self.field_pos['month'].raise_error('month without year')
            key += (self.month_num(),)
        return key

    def authors(self, field='author'):
        """Return a list of parsed author names."""
        from .algo import parse_names
        return parse_names(self[field])

    def month_num(self, field='month'):
        """Convert the month of this entry into a number in [1,12].

        This performs fairly fuzzy parsing that supports all standard
        month macro styles (and then some).

        Raises KeyError if this entry does not have the specified
        field and InputError if the field cannot be parsed.
        """
        val = self[field].strip().rstrip('.').lower()
        for i, name in enumerate(MONTHS):
            if name.startswith(val) and len(val) >= 3:
                return i + 1
        self.field_pos[field].raise_error(
            'invalid month `{}\''.format(self[field]))
<|endoftext|>


<commit_before>from werkzeug.datastructures import MultiDict

def url_update_args(**kwargs):
	'''Take the current URL and update paramters, keeping all unspecified the same.'''
	return url_update_endpoint_args(request.endpoint, **kwargs)

def url_update_endpoint_args(endpoint, **kwargs):
	'''Return the URL for passed endpoint using args from current request and kwargs.'''
	# request.args contains parameters from the query string
	# request.view_args contains parameters that matched the view signature
	args = MultiDict(request.args)
	args.update(request.view_args)
	for arg, value in kwargs.items():
		if isinstance(value, Iterable) and not isinstance(value, str):
			args.setlist(arg, value)
		else:
			args[arg] = value
	# Now set any individual args to the object instead of a list of len 1
	args = args.to_dict(flat=False)
	for key in set(args.keys()):
		if len(args[key]) == 1:
			args[key] = args[key][0]
	return url_for(endpoint, **args)
<commit_msg>bugfix - missing imports<commit_after>from collections import Iterable

from flask import request, url_for
from werkzeug.datastructures import MultiDict

def url_update_args(**kwargs):
	'''Take the current URL and update paramters, keeping all unspecified the same.'''
	return url_update_endpoint_args(request.endpoint, **kwargs)

def url_update_endpoint_args(endpoint, **kwargs):
	'''Return the URL for passed endpoint using args from current request and kwargs.'''
	# request.args contains parameters from the query string
	# request.view_args contains parameters that matched the view signature
	args = MultiDict(request.args)
	args.update(request.view_args)
	for arg, value in kwargs.items():
		if isinstance(value, Iterable) and not isinstance(value, str):
			args.setlist(arg, value)
		else:
			args[arg] = value
	# Now set any individual args to the object instead of a list of len 1
	args = args.to_dict(flat=False)
	for key in set(args.keys()):
		if len(args[key]) == 1:
			args[key] = args[key][0]
	return url_for(endpoint, **args)
<|endoftext|>


<commit_before>import time
from filtres.filtre import Filtre
import settings
import re

import logging
logger = logging.getLogger(__name__)

class FiltreNou(Filtre):

  solicitant=None
  ticket_id=None

  def es_aplicable(self):
    logger.info("Filtre de Nou")
    logger.info("Tinc un mail de %s" % self.msg.get_from())
    self.solicitant=self.get_uid()

    logger.info("UID del solicitant: %s" % self.solicitant)
    return self.solicitant!=None

  def obtenir_parametres_addicionals(self):
    defaults={"equipResolutor": settings.get("equip_resolutor_nous") }
    for item in settings.get("valors_defecte"):
        regex = re.compile(item['match'],re.IGNORECASE)
        for header_name in item['order']:
            header_value = self.msg.get_header(header_name)
            if header_value and regex.match(header_value):
                logger.info("Tinc parametres adicionals via %s" % header_name)
                defaults.update(item['defaults'])
    logger.info("Parametres addicionals: %s" % str(defaults))
    return defaults

  def filtrar(self):
    logger.info("Aplico filtre...")
    body=self.msg.get_body()
    funcio_netejar_mail_nou=settings.get("netejar_mail_nou")
    if funcio_netejar_mail_nou:
      body=funcio_netejar_mail_nou(body)
    subject=self.msg.get_subject()
    if len(subject)==0:
      subject="Ticket de %s" % self.solicitant

    if self.msg.get_reply_to()!=None:
      from_or_reply_to=self.msg.get_reply_to()
    else:
      from_or_reply_to=self.msg.get_from()

    parametres_addicionals=self.obtenir_parametres_addicionals()
    logger.info("A veure si puc crear el ticket de %s" % self.solicitant)
    descripcio=("[Tiquet creat des del correu de %s del %s a les %s]

" %
      (self.msg.get_from(),self.msg.get_date().strftime("%d/%m/%Y"),self.msg.get_date().strftime("%H:%M"))
    ) +body

    parametres={
      'assumpte':subject,
      'solicitant':self.solicitant,
      'emailSolicitant':from_or_reply_to,
      'descripcio':descripcio
    }
    parametres.update(parametres_addicionals)
    resultat=self.tickets.alta_tiquet(parametres)

    if resultat['codiRetorn']!="1":
      logger.info("Error: %s - %s" % (resultat['codiRetorn'],resultat['descripcioError']))
      return False
    logger.info("Ticket creat")

    ticket_id=resultat['codiTiquet']
    descripcio=self.afegir_attachments_canviant_body(ticket_id,self.solicitant,descripcio)
    logger.info("Attachments (si n'hi ha) afegits")

    if settings.get("assignar_data_resolucio_amb_data_creacio"):
      data_resolucio=time.strftime("%d-%m-%Y")
    else:
      data_resolucio=''

    resultat=self.tickets.modificar_tiquet(
      codiTiquet=ticket_id,
      emailSolicitant=from_or_reply_to,
      descripcio=descripcio,
      dataResol=data_resolucio
      )

    if resultat['codiRetorn']!="1":
      logger.info("Error: %s - %s" % (resultat['codiRetorn'],resultat['descripcioError']))
    else:
      logger.info("Mail modificat a %s" % self.msg.get_from())

    return True
<commit_msg>Faltaven uns ** al passar els parametres<commit_after>import time
from filtres.filtre import Filtre
import settings
import re

import logging
logger = logging.getLogger(__name__)

class FiltreNou(Filtre):

  solicitant=None
  ticket_id=None

  def es_aplicable(self):
    logger.info("Filtre de Nou")
    logger.info("Tinc un mail de %s" % self.msg.get_from())
    self.solicitant=self.get_uid()

    logger.info("UID del solicitant: %s" % self.solicitant)
    return self.solicitant!=None

  def obtenir_parametres_addicionals(self):
    defaults={"equipResolutor": settings.get("equip_resolutor_nous") }
    for item in settings.get("valors_defecte"):
        regex = re.compile(item['match'],re.IGNORECASE)
        for header_name in item['order']:
            header_value = self.msg.get_header(header_name)
            if header_value and regex.match(header_value):
                logger.info("Tinc parametres adicionals via %s" % header_name)
                defaults.update(item['defaults'])
    logger.info("Parametres addicionals: %s" % str(defaults))
    return defaults

  def filtrar(self):
    logger.info("Aplico filtre...")
    body=self.msg.get_body()
    funcio_netejar_mail_nou=settings.get("netejar_mail_nou")
    if funcio_netejar_mail_nou:
      body=funcio_netejar_mail_nou(body)
    subject=self.msg.get_subject()
    if len(subject)==0:
      subject="Ticket de %s" % self.solicitant

    if self.msg.get_reply_to()!=None:
      from_or_reply_to=self.msg.get_reply_to()
    else:
      from_or_reply_to=self.msg.get_from()

    parametres_addicionals=self.obtenir_parametres_addicionals()
    logger.info("A veure si puc crear el ticket de %s" % self.solicitant)
    descripcio=("[Tiquet creat des del correu de %s del %s a les %s]

" %
      (self.msg.get_from(),self.msg.get_date().strftime("%d/%m/%Y"),self.msg.get_date().strftime("%H:%M"))
    ) +body

    parametres={
      'assumpte':subject,
      'solicitant':self.solicitant,
      'emailSolicitant':from_or_reply_to,
      'descripcio':descripcio
    }
    parametres.update(parametres_addicionals)
    resultat=self.tickets.alta_tiquet(**parametres)

    if resultat['codiRetorn']!="1":
      logger.info("Error: %s - %s" % (resultat['codiRetorn'],resultat['descripcioError']))
      return False
    logger.info("Ticket creat")

    ticket_id=resultat['codiTiquet']
    descripcio=self.afegir_attachments_canviant_body(ticket_id,self.solicitant,descripcio)
    logger.info("Attachments (si n'hi ha) afegits")

    if settings.get("assignar_data_resolucio_amb_data_creacio"):
      data_resolucio=time.strftime("%d-%m-%Y")
    else:
      data_resolucio=''

    resultat=self.tickets.modificar_tiquet(
      codiTiquet=ticket_id,
      emailSolicitant=from_or_reply_to,
      descripcio=descripcio,
      dataResol=data_resolucio
      )

    if resultat['codiRetorn']!="1":
      logger.info("Error: %s - %s" % (resultat['codiRetorn'],resultat['descripcioError']))
    else:
      logger.info("Mail modificat a %s" % self.msg.get_from())

    return True
<|endoftext|>


<commit_before>"""
a set of defs used to manage xpi
"""

import os
import shutil
import subprocess
import stat

from jetpack import settings

def sdk_copy(sdk_source, sdk_dir=None):
	"""
	copy the sdk_source to the sdk_dir
	create cfx.sh which will set environment and call 'real' cfx
	"""
	shutil.copytree(sdk_source, sdk_dir)
	# create cfx.sh
	handle = open('%s/bin/cfx.sh' % sdk_dir, 'w')
	handle.write("""#!/bin/bash
ADDON_DIR=`pwd`
SDK_DIR=%s
cd $SDK_DIR
source $SDK_DIR/bin/activate
cd $ADDON_DIR
cfx $*""" % sdk_dir)
	handle.close()
	os.chmod('%s/bin/cfx.sh' % sdk_dir, stat.S_IXUSR|stat.S_IRUSR)

def xpi_build(sdk_dir, package_dir):
	" build xpi from source in sdk_dir "
	# set environment
	#os.environ['CUDDLEFISH_ROOT'] = sdk_dir
	old_path = os.environ['PATH']
	os.environ['PATH'] = '%s/bin:%s' % (sdk_dir, old_path)

	# create XPI
	os.chdir(package_dir)
	cfx_command = [
		'%s/bin/cfx.sh' % sdk_dir,
		'--binary=/usr/bin/xulrunner',
		'--keydir=%s/%s' % (sdk_dir, settings.KEYDIR),
		'xpi']

	try:
		process = subprocess.Popen(
						cfx_command,
						shell=False,
						stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	except subprocess.CalledProcessError:
		return HttpResponseServerError

	out = process.communicate()
	#if out[1] and not settings.DEBUG:
		#xpi_remove(sdk_dir)

	# clean up environment
	os.environ['PATH'] = old_path

	# return cfx result
	return out

def xpi_remove(sdk_dir):
	" clear directory "
	shutil.rmtree(sdk_dir)
<commit_msg>tabs to spaces<commit_after>"""
a set of defs used to manage xpi
"""

import os
import shutil
import subprocess
import stat

from jetpack import settings

def sdk_copy(sdk_source, sdk_dir=None):
    """
    copy the sdk_source to the sdk_dir
    create cfx.sh which will set environment and call 'real' cfx
    """
    shutil.copytree(sdk_source, sdk_dir)
    # create cfx.sh
    handle = open('%s/bin/cfx.sh' % sdk_dir, 'w')
    handle.write("""#!/bin/bash
ADDON_DIR=`pwd`
SDK_DIR=%s
cd $SDK_DIR
source $SDK_DIR/bin/activate
cd $ADDON_DIR
cfx $*""" % sdk_dir)
    handle.close()
    os.chmod('%s/bin/cfx.sh' % sdk_dir, stat.S_IXUSR|stat.S_IRUSR)

def xpi_build(sdk_dir, package_dir):
    " build xpi from source in sdk_dir "
    # set environment
    #os.environ['CUDDLEFISH_ROOT'] = sdk_dir
    old_path = os.environ['PATH']
    os.environ['PATH'] = '%s/bin:%s' % (sdk_dir, old_path)

    # create XPI
    os.chdir(package_dir)
    cfx_command = [
        '%s/bin/cfx.sh' % sdk_dir,
        '--binary=/usr/bin/xulrunner',
        '--keydir=%s/%s' % (sdk_dir, settings.KEYDIR),
        'xpi']

    try:
        process = subprocess.Popen(
                        cfx_command,
                        shell=False,
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError:
        return HttpResponseServerError

    out = process.communicate()
    #if out[1] and not settings.DEBUG:
        #xpi_remove(sdk_dir)

    # clean up environment
    os.environ['PATH'] = old_path

    # return cfx result
    return out

def xpi_remove(sdk_dir):
    " clear directory "
    shutil.rmtree(sdk_dir)
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-

from __future__ import with_statement
import flask
from flasfka import app
import kafka

app.config.update(dict(
    HOSTS=["localhost:9092"],
    DEFAULT_GROUP="flasfka",
    CONSUMER_TIMEOUT=0.1,
    CONSUMER_LIMIT=100
))

app.config.from_envvar("FLASFKA_CONFIG", silent=True)

def get_kafka_client():
    if not hasattr(flask.g, "kafka_client"):
        flask.g.kafka_client = kafka.KafkaClient(app.config["HOSTS"])
    return flask.g.kafka_client

def get_kafka_producer():
    if not hasattr(flask.g, "kafka_producer"):
        client = get_kafka_client()
        flask.g.kafka_producer = kafka.KeyedProducer(client)
    return flask.g.kafka_producer

def produce(topic, message, key=None):
    message = message.encode("utf-8")
    producer = get_kafka_producer()
    producer.send(topic, key, message)
    producer.stop()

def consume(topic, group, limit):
    if group is None:
        group = app.config["DEFAULT_GROUP"].encode("utf-8")
    client = get_kafka_client()
    consumer = kafka.SimpleConsumer(
        client, group, topic, iter_timeout=app.config["CONSUMER_TIMEOUT"]
        )
    res = {"group": group.decode("utf-8"), "messages": []}
    for n, message in enumerate(consumer):
        res["messages"].append(message.message.value.decode("utf-8"))
        if n >= limit - 1:
            break
    consumer.commit()
    consumer.stop()
    return res

@app.route("/<topic>/", methods=["GET", "POST"])
@app.route("/<topic>/<group_or_key>/", methods=["GET", "POST"])
def flasfka(topic, group_or_key=None):
    try:
        topic = topic.encode("utf-8")
        if group_or_key is not None:
            group_or_key = group_or_key.encode("utf-8")

        client = get_kafka_client()
        client.ensure_topic_exists(topic)

        if flask.request.method == "GET":
            limit = int(flask.request.args.get(
                "limit", app.config["CONSUMER_LIMIT"]
                ))
            group = group_or_key
            return flask.jsonify(consume(topic, group, limit))
        if flask.request.method == "POST":
            key = group_or_key
            produce(topic, flask.request.get_data(), key)
            return flask.make_response(("", 204, {}))
    except UnicodeDecodeError:
        return flask.make_response("Flafska only accepts utf-8", 400, {})

@app.after_request
def debug_cache_off(response):
    if app.config["DEBUG"]:
        response.headers["Cache-Control"] = "no-cache, max-age=0"
    return response
<commit_msg>Removing dead code<commit_after># -*- coding: utf-8 -*-

from __future__ import with_statement
import flask
from flasfka import app
import kafka

app.config.update(dict(
    HOSTS=["localhost:9092"],
    DEFAULT_GROUP="flasfka",
    CONSUMER_TIMEOUT=0.1,
    CONSUMER_LIMIT=100
))

app.config.from_envvar("FLASFKA_CONFIG", silent=True)

def get_kafka_client():
    if not hasattr(flask.g, "kafka_client"):
        flask.g.kafka_client = kafka.KafkaClient(app.config["HOSTS"])
    return flask.g.kafka_client

def get_kafka_producer():
    if not hasattr(flask.g, "kafka_producer"):
        client = get_kafka_client()
        flask.g.kafka_producer = kafka.KeyedProducer(client)
    return flask.g.kafka_producer

def produce(topic, message, key=None):
    message = message.encode("utf-8")
    producer = get_kafka_producer()
    producer.send(topic, key, message)
    producer.stop()

def consume(topic, group, limit):
    if group is None:
        group = app.config["DEFAULT_GROUP"].encode("utf-8")
    client = get_kafka_client()
    consumer = kafka.SimpleConsumer(
        client, group, topic, iter_timeout=app.config["CONSUMER_TIMEOUT"]
        )
    res = {"group": group.decode("utf-8"), "messages": []}
    for n, message in enumerate(consumer):
        res["messages"].append(message.message.value.decode("utf-8"))
        if n >= limit - 1:
            break
    consumer.commit()
    consumer.stop()
    return res

@app.route("/<topic>/", methods=["GET", "POST"])
@app.route("/<topic>/<group_or_key>/", methods=["GET", "POST"])
def flasfka(topic, group_or_key=None):
    try:
        topic = topic.encode("utf-8")
        if group_or_key is not None:
            group_or_key = group_or_key.encode("utf-8")

        client = get_kafka_client()
        client.ensure_topic_exists(topic)

        if flask.request.method == "GET":
            limit = int(flask.request.args.get(
                "limit", app.config["CONSUMER_LIMIT"]
                ))
            group = group_or_key
            return flask.jsonify(consume(topic, group, limit))
        if flask.request.method == "POST":
            key = group_or_key
            produce(topic, flask.request.get_data(), key)
            return flask.make_response(("", 204, {}))
    except UnicodeDecodeError:
        return flask.make_response("Flafska only accepts utf-8", 400, {})
<|endoftext|>


<commit_before>
from werkzeug.local import LocalProxy

from flask import current_app
from flask.signals import Namespace

from kazoo.client import KazooClient

__all__ = ('Kazoo', )

__version__ = '0.1'

_signals = Namespace()

connection_state_changed = _signals.signal('state-change')

def _get_client():
    kazoo_client = current_app._get_current_object().extensions['kazoo']['client']
    return kazoo_client

kazoo_client = LocalProxy(_get_client)

class Kazoo(object):
    """Kazoo Client support for Flask."""

    def __init__(self, app=None):
        """
        If app argument provided then initialize cqlengine connection using
        application config values.

        If no app argument provided you should do initialization later with
        :meth:`init_app` method.

        :param app: Flask application instance.

        """
        self.app = app
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        """
        Read kazoo settings from app configuration,
        setup kazoo client for application

        :param app: Flask application instance.

        """
        app.config.setdefault('ZOOKEEPER_HOSTS', '127.0.0.1:2181')
        app.config.setdefault('ZOOKEEPER_TIMEOUT', 3)

        # Put cqlengine to application extensions
        if not 'kazoo' in app.extensions:
            app.extensions['kazoo'] = {}

        # Initialize connection and store it to extensions
        self.hosts = app.config['ZOOKEEPER_HOSTS']
        self.timeout = app.config['ZOOKEEPER_TIMEOUT']

        kazoo_client = KazooClient(hosts=self.hosts)
        kazoo_client.start(self.timeout)
        kazoo_client.add_listener(self.connection_state_listener)

        app.extensions['kazoo']['client'] = kazoo_client

    def connection_state_listener(self, state):
        """Publishes state changes to a Flask signal"""
        connection_state_changed.send(self, state=state)
<commit_msg>Add config to start Kazoo blocking vs. non-blocking<commit_after>
from werkzeug.local import LocalProxy

from flask import current_app
from flask.signals import Namespace

from kazoo.client import KazooClient

__all__ = ('Kazoo', )

__version__ = '0.1'

_signals = Namespace()

connection_state_changed = _signals.signal('state-change')

def _get_client():
    kazoo_client = current_app._get_current_object().extensions['kazoo']['client']
    return kazoo_client

kazoo_client = LocalProxy(_get_client)

class Kazoo(object):
    """Kazoo Client support for Flask."""

    def __init__(self, app=None):
        """
        If app argument provided then initialize cqlengine connection using
        application config values.

        If no app argument provided you should do initialization later with
        :meth:`init_app` method.

        :param app: Flask application instance.

        """
        self.app = app
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        """
        Read kazoo settings from app configuration,
        setup kazoo client for application

        :param app: Flask application instance.

        """
        app.config.setdefault('ZOOKEEPER_HOSTS', '127.0.0.1:2181')
        app.config.setdefault('ZOOKEEPER_TIMEOUT', 3)
        app.config.setdefault('ZOOKEEPER_START_BLOCKING', False)

        # Put cqlengine to application extensions
        if not 'kazoo' in app.extensions:
            app.extensions['kazoo'] = {}

        # Initialize connection and store it to extensions
        self.hosts = app.config['ZOOKEEPER_HOSTS']
        self.timeout = app.config['ZOOKEEPER_TIMEOUT']

        kazoo_client = KazooClient(hosts=self.hosts)

        if app.config['ZOOKEEPER_START_BLOCKING']:
            kazoo_client.start(self.timeout)
        else:
            kazoo_client.start_async()

        kazoo_client.add_listener(self.connection_state_listener)

        app.extensions['kazoo']['client'] = kazoo_client

    def connection_state_listener(self, state):
        """Publishes state changes to a Flask signal"""
        connection_state_changed.send(self, state=state)
<|endoftext|>


<commit_before>import os
import fnmatch

try:
    from . import msg, shared as G, utils
    assert G and msg and utils
except ImportError:
    import msg
    import shared as G

IGNORE_FILES = ['.gitignore', '.hgignore', '.flignore']
# TODO: make this configurable
HIDDEN_WHITELIST = ['.floo'] + IGNORE_FILES
# TODO: grab global git ignores:
# gitconfig_file = popen("git config -z --get core.excludesfile", "r");

class Ignore(object):
    def __init__(self, parent, path):
        self.parent = parent
        self.ignores = []
        self.path = utils.unfuck_path(path)
        msg.debug('Initializing ignores for %s' % path)
        for ignore_file in IGNORE_FILES:
            try:
                self.load(ignore_file)
            except:
                pass

    def load(self, ignore_file):
        fd = open(os.path.join(self.path, ignore_file), 'rb')
        ignores = fd.read().decode('utf-8')
        fd.close()
        for ignore in ignores.split('\n'):
            ignore = ignore.strip()
            if len(ignore) == 0:
                continue
            if ignore[0] == '#':
                continue
            msg.debug('Adding %s to ignore patterns' % ignore)
            self.ignores.append(ignore)

    def is_ignored(self, path):
        rel_path = os.path.relpath(path, self.path)
        for pattern in self.ignores:
            base_path, file_name = os.path.split(rel_path)
            if pattern[0] == '/':
                if utils.unfuck_path(base_path) == self.path and fnmatch.fnmatch(file_name, pattern[1:]):
                    return True
            else:
                if fnmatch.fnmatch(file_name, pattern):
                    return True
                if fnmatch.fnmatch(rel_path, pattern):
                    return True
        if self.parent:
            return self.parent.is_ignored(path)
        return False

def build_ignores(path):
    current_ignore = Ignore(None, G.PROJECT_PATH)
    current_path = G.PROJECT_PATH
    starting = os.path.relpath(path, G.PROJECT_PATH)
    for p in starting.split(os.sep):
        current_path = os.path.join(current_path, p)
        if p == '..':
            raise ValueError('%s is not in project path %s' % (current_path, G.PROJECT_PATH))
        current_ignore = Ignore(current_ignore, current_path)
    return current_ignore
<commit_msg>Better ignoring of stuff. Now we know why things are ignored.<commit_after>import os
import fnmatch

try:
    from . import msg, shared as G, utils
    assert G and msg and utils
except ImportError:
    import msg
    import shared as G

IGNORE_FILES = ['.gitignore', '.hgignore', '.flignore', '.flooignore']
# TODO: make this configurable
HIDDEN_WHITELIST = ['.floo'] + IGNORE_FILES
# TODO: grab global git ignores:
# gitconfig_file = popen("git config -z --get core.excludesfile", "r");
DEFAULT_IGNORES = ['extern', 'node_modules', 'tmp', 'vendor']

def create_flooignore(path):
    flooignore = os.path.join(path, '.flooignore')
    # A very short race condition, but whatever.
    if os.path.exists(flooignore):
        return
    try:
        with open(flooignore, 'wb') as fd:
            fd.write('\n'.join(DEFAULT_IGNORES).encode('utf-8'))
    except Exception as e:
        msg.error('Error creating default .flooignore: %s' % str(e))

class Ignore(object):
    def __init__(self, parent, path):
        self.parent = parent
        self.ignores = {}
        self.path = utils.unfuck_path(path)
        msg.debug('Initializing ignores for %s' % path)
        for ignore_file in IGNORE_FILES:
            try:
                self.load(ignore_file)
            except:
                pass

    def load(self, ignore_file):
        with open(os.path.join(self.path, ignore_file), 'rb') as fd:
            ignores = fd.read().decode('utf-8')
        self.ignores[ignore_file] = []
        for ignore in ignores.split('\n'):
            ignore = ignore.strip()
            if len(ignore) == 0:
                continue
            if ignore[0] == '#':
                continue
            msg.debug('Adding %s to ignore patterns' % ignore)
            self.ignores[ignore_file].append(ignore)

    def is_ignored_message(self, path, pattern, ignore_file):
        return '%s ignored by pattern %s in %s' % (path, pattern, os.path.join(self.path, ignore_file))

    def is_ignored(self, path):
        rel_path = os.path.relpath(path, self.path)
        for ignore_file, patterns in self.ignores.items():
            for pattern in patterns:
                base_path, file_name = os.path.split(rel_path)
                if pattern[0] == '/':
                    if utils.unfuck_path(base_path) == self.path and fnmatch.fnmatch(file_name, pattern[1:]):
                        return self.is_ignored_message(path, pattern, ignore_file)
                else:
                    if fnmatch.fnmatch(file_name, pattern):
                        return self.is_ignored_message(path, pattern, ignore_file)
                    if fnmatch.fnmatch(rel_path, pattern):
                        return self.is_ignored_message(path, pattern, ignore_file)
        if self.parent:
            return self.parent.is_ignored(path)
        return False

def build_ignores(path):
    current_ignore = Ignore(None, G.PROJECT_PATH)
    current_path = G.PROJECT_PATH
    starting = os.path.relpath(path, G.PROJECT_PATH)
    for p in starting.split(os.sep):
        current_path = os.path.join(current_path, p)
        if p == '..':
            raise ValueError('%s is not in project path %s' % (current_path, G.PROJECT_PATH))
        current_ignore = Ignore(current_ignore, current_path)
    return current_ignore
<|endoftext|>


<commit_before>from __future__ import absolute_import
from itertools import product, combinations
from git.objects import Blob
from collections import defaultdict
from kenja.historage import *

def get_extends(commit, org_file_name, class_name):
    extends_path = '/'.join([org_file_name, '[CN]', class_name, 'extend'])
    try:
        extend = commit.tree / extends_path
        assert isinstance(extend, Blob)
    except KeyError:
        return None

    return extend.data_stream.read().rstrip()

def detect_pull_up_method(historage):
    pull_up_method_information = []

    for commit in historage.iter_commits(historage.head):
        for p in commit.parents:
            #pull_up_method_information.extend(detect_pullup_method_from_commit(p, commit))
            pull_up_method_information.extend(detect_shingle_pullup_method(p, commit))

    return pull_up_method_information

class Method(object):
    def __init__(self, blob, commit):
        self.blob = blob

        self.package_name = get_package(blob.path, commit)
        self.class_name = get_class(blob.path)
        self.method_name = get_method(blob.path)

    def get_full_name(self):
        if self.package_name:
            return '.'.join([self.package_name, self.class_name, self.method_name])
        else:
            return '.'.join([self.class_name, self.method_name])

    @classmethod
    def create_from_blob(cls, blob, commit):
        if is_method_body(blob.path):
            return cls(blob, commit)
        else:
            return None

    def __str__(self):
        return self.get_full_name()

class SubclassMethod(Method):
    def __init__(self, blob, commit):
        super(SubclassMethod, self).__init__(blob, commit)

        split_path = blob.path.split('/')
        self.extend = get_extends(commit, split_path[0], self.class_name)

def detect_shingle_pullup_method(old_commit, new_commit):
    result = []
    #pullup_method_candidates = default

    diff_index = old_commit.diff(new_commit, create_patch=False)

    #method_added_classes =
    added_methods = defaultdict(list)
    delted_methods = defaultdict(lambda : defaultdict(list))
    for diff in diff_index.iter_change_type('A'):
        new_blob_path = diff.b_blob.path
        new_method = Method.create_from_blob(diff.b_blob, new_commit)
        if new_method:
            added_methods[new_method.class_name].append(new_method)

    for diff in diff_index.iter_change_type('D'):
        deleted_blob_path = diff.a_blob.path

        subclass_method = SubclassMethod.create_from_blob(diff.a_blob, old_commit)

        if subclass_method:
            if not subclass_method.extend:
                continue

            if subclass_method.extend in added_methods.keys():
                #print extend
                delted_methods[subclass_method.extend][subclass_method.class_name].append(subclass_method)

    for super_class, v in delted_methods.iteritems():
        if super_class not in added_methods:
            print '%s does\'nt have a deleted method' % (super_class)
            continue
        for dst_method in added_methods[super_class]:
            for src_class in v.keys():
                for src_method in v[src_class]:
                    print '[from] %s [to] %s' % (src_method, dst_method)
    return result

def detect_pullup_method_from_commit(old_commit, new_commit):
    result = []
    #pullup_method_candidates = default

    diff_index = old_commit.diff(new_commit, create_patch=False)

    #method_added_classes =
    added_methods = defaultdict(list)
    delted_methods = defaultdict(lambda : defaultdict(list))
    for diff in diff_index.iter_change_type('A'):
        if is_method_body(diff.b_blob.path):
            c = get_class(diff.b_blob.path)
            added_methods[c].append(get_method(diff.b_blob.path))

    for diff in diff_index.iter_change_type('D'):
        if is_method_body(diff.a_blob.path):
            c = get_class(diff.a_blob.path)
            split_path =diff.a_blob.path.split('/')
            extend = get_extends(new_commit, split_path[0], c)
            if not extend:
                continue

            extend = extend.rstrip()
            if extend in added_methods.keys():
                #print extend
                delted_methods[extend][c].append(get_method(diff.a_blob.path))

    for k, v in delted_methods.items():
        pull_up_target = k
        #method_deleted_classes = set()
        #for c, m in v.items():
        #    method_deleted_classes.add(c)
        if len(v.keys()) > 2:
            print old_commit, new_commit, pull_up_target, v
            print '[keys]:', v.keys()
            for p in combinations(v.keys(), 2):
                print p
                for p2 in product(v[p[0]], v[p[1]]):
                    print p, p2

    return result

<commit_msg>removed uncommented code<commit_after>from __future__ import absolute_import
from itertools import product, combinations
from git.objects import Blob
from collections import defaultdict
from kenja.historage import *

def get_extends(commit, org_file_name, class_name):
    extends_path = '/'.join([org_file_name, '[CN]', class_name, 'extend'])
    try:
        extend = commit.tree / extends_path
        assert isinstance(extend, Blob)
    except KeyError:
        return None

    return extend.data_stream.read().rstrip()

def detect_pull_up_method(historage):
    pull_up_method_information = []

    for commit in historage.iter_commits(historage.head):
        for p in commit.parents:
            #pull_up_method_information.extend(detect_pullup_method_from_commit(p, commit))
            pull_up_method_information.extend(detect_shingle_pullup_method(p, commit))

    return pull_up_method_information

class Method(object):
    def __init__(self, blob, commit):
        self.blob = blob

        self.package_name = get_package(blob.path, commit)
        self.class_name = get_class(blob.path)
        self.method_name = get_method(blob.path)

    def get_full_name(self):
        if self.package_name:
            return '.'.join([self.package_name, self.class_name, self.method_name])
        else:
            return '.'.join([self.class_name, self.method_name])

    @classmethod
    def create_from_blob(cls, blob, commit):
        if is_method_body(blob.path):
            return cls(blob, commit)
        else:
            return None

    def __str__(self):
        return self.get_full_name()

class SubclassMethod(Method):
    def __init__(self, blob, commit):
        super(SubclassMethod, self).__init__(blob, commit)

        split_path = blob.path.split('/')
        self.extend = get_extends(commit, split_path[0], self.class_name)

def detect_shingle_pullup_method(old_commit, new_commit):
    result = []
    #pullup_method_candidates = default

    diff_index = old_commit.diff(new_commit, create_patch=False)

    #method_added_classes =
    added_methods = defaultdict(list)
    delted_methods = defaultdict(lambda : defaultdict(list))
    for diff in diff_index.iter_change_type('A'):
        new_blob_path = diff.b_blob.path
        new_method = Method.create_from_blob(diff.b_blob, new_commit)
        if new_method:
            added_methods[new_method.class_name].append(new_method)

    for diff in diff_index.iter_change_type('D'):
        deleted_blob_path = diff.a_blob.path

        subclass_method = SubclassMethod.create_from_blob(diff.a_blob, old_commit)

        if subclass_method:
            if not subclass_method.extend:
                continue

            if subclass_method.extend in added_methods.keys():
                delted_methods[subclass_method.extend][subclass_method.class_name].append(subclass_method)

    for super_class, v in delted_methods.iteritems():
        if super_class not in added_methods:
            print '%s does\'nt have a deleted method' % (super_class)
            continue
        for dst_method in added_methods[super_class]:
            for src_class in v.keys():
                for src_method in v[src_class]:
                    print '[from] %s [to] %s' % (src_method, dst_method)
    return result

def detect_pullup_method_from_commit(old_commit, new_commit):
    result = []
    #pullup_method_candidates = default

    diff_index = old_commit.diff(new_commit, create_patch=False)

    #method_added_classes =
    added_methods = defaultdict(list)
    delted_methods = defaultdict(lambda : defaultdict(list))
    for diff in diff_index.iter_change_type('A'):
        if is_method_body(diff.b_blob.path):
            c = get_class(diff.b_blob.path)
            added_methods[c].append(get_method(diff.b_blob.path))

    for diff in diff_index.iter_change_type('D'):
        if is_method_body(diff.a_blob.path):
            c = get_class(diff.a_blob.path)
            split_path =diff.a_blob.path.split('/')
            extend = get_extends(new_commit, split_path[0], c)
            if not extend:
                continue

            extend = extend.rstrip()
            if extend in added_methods.keys():
                #print extend
                delted_methods[extend][c].append(get_method(diff.a_blob.path))

    for k, v in delted_methods.items():
        pull_up_target = k
        #method_deleted_classes = set()
        #for c, m in v.items():
        #    method_deleted_classes.add(c)
        if len(v.keys()) > 2:
            print old_commit, new_commit, pull_up_target, v
            print '[keys]:', v.keys()
            for p in combinations(v.keys(), 2):
                print p
                for p2 in product(v[p[0]], v[p[1]]):
                    print p, p2

    return result

<|endoftext|>


<commit_before>import abc
import httplib as http

from framework.auth import Auth
from framework.exceptions import HTTPError
from framework.exceptions import PermissionsError

from website.oauth.models import ExternalAccount, ExternalProvider

class CitationsOauthProvider(ExternalProvider):

    _client = None

    @abc.abstractproperty
    def serializer(self):
        pass

    @abc.abstractmethod
    def _get_client(self):
        pass

    @abc.abstractmethod
    def _get_folders(self):
        pass

    @abc.abstractmethod
    def _verify_client_validity(self):
        pass

    @abc.abstractmethod
    def _folder_metadata(self, folder_id):
        pass

    @abc.abstractmethod
    def _citations_for_folder(self, folder_id):
        pass

    @abc.abstractmethod
    def _citations_for_user(self, folder_id):
        pass

    @property
    def client(self):
        """An API session with <provider_name>"""
        if not self._client:
            self._client = self._get_client()
            self._verify_client_validity()

        return self._client

    def citation_lists(self, extract_folder):
        """List of CitationList objects, derived from Mendeley folders"""

        folders = self._get_folders()
        # TODO: Verify OAuth access to each folder
        all_documents = self.serializer.serialized_root_folder

        serialized_folders = [
            extract_folder(each)
            for each in folders
        ]
        return [all_documents] + serialized_folders

    def get_list(self, list_id=None):
        """Get a single CitationList
        :param str list_id: ID for a folder. Optional.
        :return CitationList: CitationList for the folder, or for all documents
        """
        if not list_id or list_id == 'ROOT':
            return self._citations_for_user()

        return self._citations_for_folder(list_id)

class CitationsProvider(object):

    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def serializer(self):
        pass

    @abc.abstractproperty
    def provider_name(self):
        pass

    def check_credentials(self, node_addon):
        """Checkes validity of credentials"""
        valid = True
        if node_addon.api.account:
            try:
                node_addon.api.client
            except HTTPError as err:
                if err.code == 403:
                    valid = False
                else:
                    raise err

        return valid

    def user_accounts(self, user):
        """ Gets a list of the accounts authorized by 'user' """
        return {
            'accounts': [
                self.serializer(
                    user_settings=user.get_addon(self.provider_name) if user else None
                ).serialize_account(each)
                for each in user.external_accounts.all()
                if each.provider == self.provider_name
            ]
        }

    def set_config(self, node_addon, user, external_list_id, external_list_name, auth):
        """ Changes folder associated with addon and logs event"""
        # Ensure request has all required information
        # Tell the user's addon settings that this node is connecting
        node_addon.user_settings.grant_oauth_access(
            node=node_addon.owner,
            external_account=node_addon.external_account,
            metadata={'folder': external_list_id}
        )
        node_addon.user_settings.save()

        # update this instance
        node_addon.list_id = external_list_id
        node_addon.save()

        node_addon.owner.add_log(
            '{0}_folder_selected'.format(self.provider_name),
            params={
                'project': node_addon.owner.parent_id,
                'node': node_addon.owner._id,
                'folder_id': external_list_id,
                'folder_name': external_list_name,
            },
            auth=auth,
        )

    def add_user_auth(self, node_addon, user, external_account_id):
        """Adds authorization to a node
        if the user has authorization to grant"""
        external_account = ExternalAccount.load(external_account_id)

        if not user.external_accounts.filter(id=external_account.id).all():
            raise HTTPError(http.FORBIDDEN)

        try:
            node_addon.set_auth(external_account, user)
        except PermissionsError:
            raise HTTPError(http.FORBIDDEN)

        result = self.serializer(
            node_settings=node_addon,
            user_settings=user.get_addon(self.provider_name),
        ).serialized_node_settings
        result['validCredentials'] = self.check_credentials(node_addon)
        return {'result': result}

    def remove_user_auth(self, node_addon, user):
        """Removes authorization from a node """
        node_addon.deauthorize(auth=Auth(user))
        node_addon.reload()
        result = self.serializer(
            node_settings=node_addon,
            user_settings=user.get_addon(self.provider_name),
        ).serialized_node_settings

        return {'result': result}

    def widget(self, node_addon):
        """Serializes settting needed to build the widget"""
        ret = node_addon.config.to_json()
        ret.update({
            'complete': node_addon.complete,
            'list_id': node_addon.list_id,
        })
        return ret

    def _extract_folder(self, folder):
        """Returns serialization of citations folder """
        folder = self._folder_to_dict(folder)
        return {
            'name': folder['name'],
            'provider_list_id': folder['list_id'],
            'id': folder['id'],
            'parent_list_id': folder.get('parent_id', None)
        }

    @abc.abstractmethod
    def _folder_to_dict(self, data):
        pass

    def _folder_id(self, node_addon):
        return node_addon.list_id

    def citation_list(self, node_addon, user, list_id, show='all'):
        """Returns a list of citations"""
        attached_list_id = self._folder_id(node_addon)
        account_folders = node_addon.api.citation_lists(self._extract_folder)

        # Folders with 'parent_list_id'==None are children of 'All Documents'
        for folder in account_folders:
            if not folder.get('parent_list_id'):
                folder['parent_list_id'] = 'ROOT'

        node_account = node_addon.external_account
        user_is_owner = user.external_accounts.filter(id=node_account.id).exists()

        # verify this list is the attached list or its descendant
        if not user_is_owner and (list_id != attached_list_id and attached_list_id is not None):
            folders = {
                (each['provider_list_id'] or 'ROOT'): each
                for each in account_folders
            }
            if list_id is None:
                ancestor_id = 'ROOT'
            else:
                ancestor_id = folders[list_id].get('parent_list_id')

            while ancestor_id != attached_list_id:
                if ancestor_id is '__':
                    raise HTTPError(http.FORBIDDEN)
                ancestor_id = folders[ancestor_id].get('parent_list_id')

        contents = []
        if list_id is None:
            contents = [node_addon.root_folder]
        else:
            user_settings = user.get_addon(self.provider_name) if user else None
            if show in ('all', 'folders'):
                contents += [
                    self.serializer(
                        node_settings=node_addon,
                        user_settings=user_settings,
                    ).serialize_folder(each)
                    for each in account_folders
                    if each.get('parent_list_id') == list_id
                ]

            if show in ('all', 'citations'):
                contents += [
                    self.serializer(
                        node_settings=node_addon,
                        user_settings=user_settings,
                    ).serialize_citation(each)
                    for each in node_addon.api.get_list(list_id)
                ]

        return {
            'contents': contents
        }
<commit_msg>Make list comp into a filter h/t @erinspace (#6429)<commit_after>import abc
import httplib as http

from framework.auth import Auth
from framework.exceptions import HTTPError
from framework.exceptions import PermissionsError

from website.oauth.models import ExternalAccount, ExternalProvider

class CitationsOauthProvider(ExternalProvider):

    _client = None

    @abc.abstractproperty
    def serializer(self):
        pass

    @abc.abstractmethod
    def _get_client(self):
        pass

    @abc.abstractmethod
    def _get_folders(self):
        pass

    @abc.abstractmethod
    def _verify_client_validity(self):
        pass

    @abc.abstractmethod
    def _folder_metadata(self, folder_id):
        pass

    @abc.abstractmethod
    def _citations_for_folder(self, folder_id):
        pass

    @abc.abstractmethod
    def _citations_for_user(self, folder_id):
        pass

    @property
    def client(self):
        """An API session with <provider_name>"""
        if not self._client:
            self._client = self._get_client()
            self._verify_client_validity()

        return self._client

    def citation_lists(self, extract_folder):
        """List of CitationList objects, derived from Mendeley folders"""

        folders = self._get_folders()
        # TODO: Verify OAuth access to each folder
        all_documents = self.serializer.serialized_root_folder

        serialized_folders = [
            extract_folder(each)
            for each in folders
        ]
        return [all_documents] + serialized_folders

    def get_list(self, list_id=None):
        """Get a single CitationList
        :param str list_id: ID for a folder. Optional.
        :return CitationList: CitationList for the folder, or for all documents
        """
        if not list_id or list_id == 'ROOT':
            return self._citations_for_user()

        return self._citations_for_folder(list_id)

class CitationsProvider(object):

    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def serializer(self):
        pass

    @abc.abstractproperty
    def provider_name(self):
        pass

    def check_credentials(self, node_addon):
        """Checkes validity of credentials"""
        valid = True
        if node_addon.api.account:
            try:
                node_addon.api.client
            except HTTPError as err:
                if err.code == 403:
                    valid = False
                else:
                    raise err

        return valid

    def user_accounts(self, user):
        """ Gets a list of the accounts authorized by 'user' """
        return {
            'accounts': [
                self.serializer(
                    user_settings=user.get_addon(self.provider_name) if user else None
                ).serialize_account(each)
                for each in user.external_accounts.filter(provider=self.provider_name)
            ]
        }

    def set_config(self, node_addon, user, external_list_id, external_list_name, auth):
        """ Changes folder associated with addon and logs event"""
        # Ensure request has all required information
        # Tell the user's addon settings that this node is connecting
        node_addon.user_settings.grant_oauth_access(
            node=node_addon.owner,
            external_account=node_addon.external_account,
            metadata={'folder': external_list_id}
        )
        node_addon.user_settings.save()

        # update this instance
        node_addon.list_id = external_list_id
        node_addon.save()

        node_addon.owner.add_log(
            '{0}_folder_selected'.format(self.provider_name),
            params={
                'project': node_addon.owner.parent_id,
                'node': node_addon.owner._id,
                'folder_id': external_list_id,
                'folder_name': external_list_name,
            },
            auth=auth,
        )

    def add_user_auth(self, node_addon, user, external_account_id):
        """Adds authorization to a node
        if the user has authorization to grant"""
        external_account = ExternalAccount.load(external_account_id)

        if not user.external_accounts.filter(id=external_account.id).all():
            raise HTTPError(http.FORBIDDEN)

        try:
            node_addon.set_auth(external_account, user)
        except PermissionsError:
            raise HTTPError(http.FORBIDDEN)

        result = self.serializer(
            node_settings=node_addon,
            user_settings=user.get_addon(self.provider_name),
        ).serialized_node_settings
        result['validCredentials'] = self.check_credentials(node_addon)
        return {'result': result}

    def remove_user_auth(self, node_addon, user):
        """Removes authorization from a node """
        node_addon.deauthorize(auth=Auth(user))
        node_addon.reload()
        result = self.serializer(
            node_settings=node_addon,
            user_settings=user.get_addon(self.provider_name),
        ).serialized_node_settings

        return {'result': result}

    def widget(self, node_addon):
        """Serializes settting needed to build the widget"""
        ret = node_addon.config.to_json()
        ret.update({
            'complete': node_addon.complete,
            'list_id': node_addon.list_id,
        })
        return ret

    def _extract_folder(self, folder):
        """Returns serialization of citations folder """
        folder = self._folder_to_dict(folder)
        return {
            'name': folder['name'],
            'provider_list_id': folder['list_id'],
            'id': folder['id'],
            'parent_list_id': folder.get('parent_id', None)
        }

    @abc.abstractmethod
    def _folder_to_dict(self, data):
        pass

    def _folder_id(self, node_addon):
        return node_addon.list_id

    def citation_list(self, node_addon, user, list_id, show='all'):
        """Returns a list of citations"""
        attached_list_id = self._folder_id(node_addon)
        account_folders = node_addon.api.citation_lists(self._extract_folder)

        # Folders with 'parent_list_id'==None are children of 'All Documents'
        for folder in account_folders:
            if not folder.get('parent_list_id'):
                folder['parent_list_id'] = 'ROOT'

        node_account = node_addon.external_account
        user_is_owner = user.external_accounts.filter(id=node_account.id).exists()

        # verify this list is the attached list or its descendant
        if not user_is_owner and (list_id != attached_list_id and attached_list_id is not None):
            folders = {
                (each['provider_list_id'] or 'ROOT'): each
                for each in account_folders
            }
            if list_id is None:
                ancestor_id = 'ROOT'
            else:
                ancestor_id = folders[list_id].get('parent_list_id')

            while ancestor_id != attached_list_id:
                if ancestor_id is '__':
                    raise HTTPError(http.FORBIDDEN)
                ancestor_id = folders[ancestor_id].get('parent_list_id')

        contents = []
        if list_id is None:
            contents = [node_addon.root_folder]
        else:
            user_settings = user.get_addon(self.provider_name) if user else None
            if show in ('all', 'folders'):
                contents += [
                    self.serializer(
                        node_settings=node_addon,
                        user_settings=user_settings,
                    ).serialize_folder(each)
                    for each in account_folders
                    if each.get('parent_list_id') == list_id
                ]

            if show in ('all', 'citations'):
                contents += [
                    self.serializer(
                        node_settings=node_addon,
                        user_settings=user_settings,
                    ).serialize_citation(each)
                    for each in node_addon.api.get_list(list_id)
                ]

        return {
            'contents': contents
        }
<|endoftext|>