from collections.abc import Callable
from copy import deepcopy
from dataclasses import dataclass
from dataclasses import field
from typing import Any
from bibtexparser.library import Library
from bibtexparser.model import Block
from bibtexparser.model import Entry
from bibtexparser.model import ExplicitComment
from bibtexparser.model import ImplicitComment
from bibtexparser.model import Preamble
from bibtexparser.model import String
from .middleware import LibraryMiddleware
DEFAULT_BLOCK_TYPE_ORDER = (String, Preamble, Entry, ImplicitComment, ExplicitComment)
@dataclass
class _BlockChunk:
"""Data-Structure reflecting zero or more comments together with a block."""
# The blocks (comments and the main block) are stored in the order they were parsed.
blocks: list[Block] = field(default_factory=list)
@property
def main_block(self) -> Block:
"""Returns the main (i.e., last, non-comment) block of this chunk."""
try:
return self.blocks[-1]
except IndexError:
raise RuntimeError(
"Block chunk must contain at least one block. "
"This is a bug in bibtexparser, please report it."
)
[docs]
class SortBlocksMiddleware(LibraryMiddleware):
"""Sorts the blocks of a library by a user-provided sort key.
This middleware works like Pythons built-in sorting
(the ``key`` and ``reverse`` arguments behave as in :func:`sorted`):
The ``key`` callable is applied to each block and the blocks are
sorted by the returned values.
Example: To sort entries by their ``year`` field, with all non-entry
blocks (strings, preambles, ...) and year-less entries on top::
from bibtexparser.middlewares import SortBlocksMiddleware
from bibtexparser.model import Entry
def by_year(block):
if isinstance(block, Entry) and "year" in block:
return (1, int(block["year"]))
return (0, 0)
middleware = SortBlocksMiddleware(key=by_year)
Hints regarding the ``key`` callable:
- It must accept every block it may be called with (see below)
and the returned values must be mutually comparable.
Returning tuples - as in the example above - is a simple way
to achieve this for libraries with mixed block types,
and also allows hierarchical sorting criteria.
- The sort is stable: blocks for which the key returns equal values
remain in their original relative order.
- The key should be pure (deterministic and without side-effects).
- If you have a comparator function (``compare(block_1, block_2) -> int``)
instead of a key function, wrap it with :func:`functools.cmp_to_key`.
Comment handling: if ``preserve_comments_on_top`` is True (default),
comments remain directly above the consecutive non-comment block
and the key is only called with said non-comment block.
(Exception: for comments at the very end of the library - not followed
by any non-comment block - the key is called with the last comment.)
If ``preserve_comments_on_top`` is False, comments are sorted
like all other blocks, and the key must thus handle comment blocks, too.
"""
def __init__(
self,
key: Callable[[Block], Any],
reverse: bool = False,
preserve_comments_on_top: bool = True,
):
"""
:param key: Callable mapping a block to a sort key, as in :func:`sorted`.
See the class docstring for requirements and an example.
:param reverse: If True, sort in descending order.
:param preserve_comments_on_top: If True, comments remain above
the following non-comment block (sorted as one unit).
"""
self._key = key
self._reverse = reverse
self._preserve_comments_on_top = preserve_comments_on_top
# In-place modification is not yet supported, we make this explicit here,
super().__init__(allow_inplace_modification=False)
@staticmethod
def _block_chunks(blocks: list[Block]) -> list[_BlockChunk]:
block_chunks = []
current_chunk = _BlockChunk()
for block in blocks:
current_chunk.blocks.append(block)
if not (isinstance(block, ExplicitComment) or isinstance(block, ImplicitComment)):
# We added a non-comment block, hence we finish the chunk and
# start a new one
block_chunks.append(current_chunk)
current_chunk = _BlockChunk()
if current_chunk.blocks:
# That would be a chunk with only comments, but we add it at the end for completeness
block_chunks.append(current_chunk)
return block_chunks
# docstr-coverage: inherited
[docs]
class SortBlocksByTypeAndKeyMiddleware(SortBlocksMiddleware):
"""Sorts the blocks of a library by type and key. Optionally, comments remain above same block."""
def __init__(
self,
block_type_order: tuple[type[Block], ...] = DEFAULT_BLOCK_TYPE_ORDER,
preserve_comments_on_top: bool = True,
):
self._verify_all_types_are_block_types(block_type_order)
self._block_type_order = block_type_order
super().__init__(
key=self._type_and_key_sort_key,
preserve_comments_on_top=preserve_comments_on_top,
)
@staticmethod
def _verify_all_types_are_block_types(sort_order):
for t in sort_order:
if not issubclass(t, Block):
raise ValueError(
"Sort order must only contain Block subclasses, " f"but got {str(t)}"
)
def _type_and_key_sort_key(self, block: Block) -> tuple[int, str]:
"""Sort key for blocks. Based on (block type, string-or-entry-key)."""
try:
type_index = self._block_type_order.index(type(block))
except ValueError:
# If the block type is not in the order list, put it at the end
type_index = len(self._block_type_order)
return type_index, getattr(block, "key", "")