diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2b7faf..6e55ce1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -25,35 +25,31 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 26.5.1 hooks: - id: black args: [--safe, --quiet, --line-length=100] - repo: https://github.com/PyCQA/autoflake - rev: v2.3.0 + rev: v2.3.3 hooks: - id: autoflake args: [--in-place, --remove-unused-variable] - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 8.0.1 hooks: - id: isort name: isort args: ["--force-single-line", "--line-length=100", "--profile=black"] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.21.2 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py310-plus] - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + rev: 7.3.0 hooks: - id: flake8 args: ["--ignore=E722,W503,E203", --max-line-length=110, "--per-file-ignores=*/__init__.py:F401"] -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.5.0 - hooks: - - id: setup-cfg-fmt - repo: https://github.com/HunterMcGushion/docstr_coverage rev: v2.3.2 hooks: diff --git a/bibtexparser/entrypoint.py b/bibtexparser/entrypoint.py index d55ce81..52a2505 100644 --- a/bibtexparser/entrypoint.py +++ b/bibtexparser/entrypoint.py @@ -1,11 +1,8 @@ import codecs import warnings -from typing import Iterable -from typing import List +from collections.abc import Iterable from typing import Optional from typing import TextIO -from typing import Tuple -from typing import Union from .library import Library from .middlewares.middleware import Middleware @@ -17,9 +14,9 @@ def _build_parse_stack( - parse_stack: Optional[Iterable[Middleware]], - append_middleware: Optional[Iterable[Middleware]], -) -> List[Middleware]: + parse_stack: Iterable[Middleware] | None, + append_middleware: Iterable[Middleware] | None, +) -> list[Middleware]: if parse_stack is not None and append_middleware is not None: raise ValueError( "Provided both parse_stack and append_middleware. " @@ -47,9 +44,9 @@ def _build_parse_stack( def _build_unparse_stack( - unparse_stack: Optional[Iterable[Middleware]], - prepend_middleware: Optional[Iterable[Middleware]], -) -> List[Middleware]: + unparse_stack: Iterable[Middleware] | None, + prepend_middleware: Iterable[Middleware] | None, +) -> list[Middleware]: if unparse_stack is not None and prepend_middleware is not None: raise ValueError( "Provided both unparse_stack and prepend_middleware. " @@ -77,11 +74,11 @@ def _build_unparse_stack( def _handle_deprecated_write_params( - unparse_stack: Optional[Iterable[Middleware]], - prepend_middleware: Optional[Iterable[Middleware]], + unparse_stack: Iterable[Middleware] | None, + prepend_middleware: Iterable[Middleware] | None, kwargs: dict, function_name: str, -) -> Tuple[Optional[Iterable[Middleware]], Optional[Iterable[Middleware]]]: +) -> tuple[Iterable[Middleware] | None, Iterable[Middleware] | None]: """Handle deprecated parameter names for write functions. :param unparse_stack: Current unparse_stack value @@ -124,9 +121,9 @@ def _handle_deprecated_write_params( def parse_string( bibtex_str: str, - parse_stack: Optional[Iterable[Middleware]] = None, - append_middleware: Optional[Iterable[Middleware]] = None, - library: Optional[Library] = None, + parse_stack: Iterable[Middleware] | None = None, + append_middleware: Iterable[Middleware] | None = None, + library: Library | None = None, ) -> Library: """Parse a BibTeX string. @@ -156,8 +153,8 @@ def parse_string( def parse_file( path: str, - parse_stack: Optional[Iterable[Middleware]] = None, - append_middleware: Optional[Iterable[Middleware]] = None, + parse_stack: Iterable[Middleware] | None = None, + append_middleware: Iterable[Middleware] | None = None, encoding: str = "UTF-8", ) -> Library: """Parse a BibTeX file @@ -188,11 +185,11 @@ def parse_file( def write_file( - file: Union[str, TextIO], + file: str | TextIO, library: Library, - unparse_stack: Optional[Iterable[Middleware]] = None, - prepend_middleware: Optional[Iterable[Middleware]] = None, - bibtex_format: Optional[BibtexFormat] = None, + unparse_stack: Iterable[Middleware] | None = None, + prepend_middleware: Iterable[Middleware] | None = None, + bibtex_format: BibtexFormat | None = None, encoding: str = "UTF-8", **kwargs, ) -> None: @@ -230,8 +227,8 @@ def write_file( def write_string( library: Library, - unparse_stack: Optional[Iterable[Middleware]] = None, - prepend_middleware: Optional[Iterable[Middleware]] = None, + unparse_stack: Iterable[Middleware] | None = None, + prepend_middleware: Iterable[Middleware] | None = None, bibtex_format: Optional["BibtexFormat"] = None, **kwargs, ) -> str: diff --git a/bibtexparser/exceptions.py b/bibtexparser/exceptions.py index 772203e..fb0a68c 100644 --- a/bibtexparser/exceptions.py +++ b/bibtexparser/exceptions.py @@ -1,7 +1,3 @@ -from typing import List -from typing import Optional - - class ParsingException(Exception): """Generic Exception for parsing errors""" @@ -25,7 +21,7 @@ def __init__( self, abort_reason: str, # Not provided if end of file is reached - end_index: Optional[int] = None, + end_index: int | None = None, ): self.abort_reason = abort_reason self.end_index = end_index @@ -60,6 +56,6 @@ def __init__(self, first_match, expected_match, second_match): class PartialMiddlewareException(ParsingException): """Exception raised when a middleware could not be fully applied.""" - def __init__(self, reasons: List[str]): + def __init__(self, reasons: list[str]): reasons_string = "\n\n=====\n\n".join(reasons) super().__init__(f"Middleware could not be fully applied: {reasons_string}") diff --git a/bibtexparser/library.py b/bibtexparser/library.py index fd50471..de33f8d 100644 --- a/bibtexparser/library.py +++ b/bibtexparser/library.py @@ -1,7 +1,3 @@ -from typing import Dict -from typing import List -from typing import Union - from .model import Block from .model import DuplicateBlockKeyBlock from .model import Entry @@ -19,7 +15,7 @@ class Library: def __init__( self, - blocks: Union[List[Block], None] = None, + blocks: list[Block] | None = None, fail_on_duplicate_key: bool = True, ): self._blocks = [] @@ -28,7 +24,7 @@ def __init__( if blocks is not None: self.add(blocks, fail_on_duplicate_key=fail_on_duplicate_key) - def add(self, blocks: Union[List[Block], Block], fail_on_duplicate_key: bool = True): + def add(self, blocks: list[Block] | Block, fail_on_duplicate_key: bool = True): """Add blocks to library. The adding is key-safe, i.e., it is made sure that no duplicate keys are added @@ -64,7 +60,7 @@ def add(self, blocks: Union[List[Block], Block], fail_on_duplicate_key: bool = T block = self._add_to_dicts(block) self._blocks.append(block) - def _find_duplicate_keys(self, blocks: List[Block]) -> List[str]: + def _find_duplicate_keys(self, blocks: list[Block]) -> list[str]: """Keys of blocks that would become duplicates when added to the library.""" duplicate_keys = [] seen_entry_keys = set(self._entries_by_key) @@ -91,7 +87,7 @@ def _block_index(self, block: Block) -> int: # No identity match; fall back to equality (raises ValueError if not found). return self._blocks.index(block) - def remove(self, blocks: Union[List[Block], Block]): + def remove(self, blocks: list[Block] | Block): """Remove blocks from library. If equal duplicate blocks exist in the library, the exact (identical) @@ -143,9 +139,7 @@ def replace(self, old_block: Block, new_block: Block, fail_on_duplicate_key: boo raise ValueError("Duplicate key found.") @staticmethod - def _cast_to_duplicate( - prev_block_with_same_key: Union[Entry, String], duplicate: Union[Entry, String] - ): + def _cast_to_duplicate(prev_block_with_same_key: Entry | String, duplicate: Entry | String): if not ( isinstance(prev_block_with_same_key, type(duplicate)) or isinstance(duplicate, type(prev_block_with_same_key)) @@ -196,44 +190,44 @@ def _add_to_dicts(self, block): return block @property - def blocks(self) -> List[Block]: + def blocks(self) -> list[Block]: """All blocks in the library, preserving order of insertion.""" return self._blocks @property - def failed_blocks(self) -> List[ParsingFailedBlock]: + def failed_blocks(self) -> list[ParsingFailedBlock]: """All blocks that could not be parsed, preserving order of insertion.""" return [b for b in self._blocks if isinstance(b, ParsingFailedBlock)] @property - def strings(self) -> List[String]: + def strings(self) -> list[String]: """All @string blocks in the library, preserving order of insertion.""" return list(self._strings_by_key.values()) @property - def strings_dict(self) -> Dict[str, String]: + def strings_dict(self) -> dict[str, String]: """Dict representation of all @string blocks in the library.""" return self._strings_by_key.copy() @property - def entries(self) -> List[Entry]: + def entries(self) -> list[Entry]: """All entry (@article, ...) blocks in the library, preserving order of insertion.""" # Note: Taking this from the entries dict would be faster, but does not preserve order # e.g. in cases where `replace` has been called. return [b for b in self._blocks if isinstance(b, Entry)] @property - def entries_dict(self) -> Dict[str, Entry]: + def entries_dict(self) -> dict[str, Entry]: """Dict representation of all entry blocks in the library.""" return self._entries_by_key.copy() @property - def preambles(self) -> List[Preamble]: + def preambles(self) -> list[Preamble]: """All @preamble blocks in the library, preserving order of insertion.""" return [block for block in self._blocks if isinstance(block, Preamble)] @property - def comments(self) -> List[Union[ExplicitComment, ImplicitComment]]: + def comments(self) -> list[ExplicitComment | ImplicitComment]: """All comment blocks in the library, preserving order of insertion.""" return [ block for block in self._blocks if isinstance(block, (ExplicitComment, ImplicitComment)) diff --git a/bibtexparser/middlewares/enclosing.py b/bibtexparser/middlewares/enclosing.py index 5bc172b..a3c09ca 100644 --- a/bibtexparser/middlewares/enclosing.py +++ b/bibtexparser/middlewares/enclosing.py @@ -1,7 +1,3 @@ -from typing import Optional -from typing import Tuple -from typing import Union - from bibtexparser.library import Library from bibtexparser.model import Entry from bibtexparser.model import Field @@ -53,7 +49,7 @@ def metadata_key(cls) -> str: return REMOVED_ENCLOSING_KEY @staticmethod - def _strip_enclosing(value: str) -> Tuple[str, Union[str, None]]: + def _strip_enclosing(value: str) -> tuple[str, str | None]: value = value.strip() if value.startswith("{") and value.endswith("}"): return value[1:-1], "{" @@ -144,9 +140,9 @@ def metadata_key(cls) -> str: def _enclose( self, value: str, - metadata_enclosing: Optional[str], + metadata_enclosing: str | None, apply_int_rule: bool, - demanded_enclosing: Optional[str] = None, + demanded_enclosing: str | None = None, ) -> str: enclosing = self._default_enclosing if demanded_enclosing is not None: diff --git a/bibtexparser/middlewares/fieldkeys.py b/bibtexparser/middlewares/fieldkeys.py index 482507b..ea66812 100644 --- a/bibtexparser/middlewares/fieldkeys.py +++ b/bibtexparser/middlewares/fieldkeys.py @@ -1,7 +1,4 @@ import logging -from typing import Dict -from typing import List -from typing import Set from bibtexparser.library import Library from bibtexparser.model import Entry @@ -29,8 +26,8 @@ def __init__(self, allow_inplace_modification: bool = True): # docstr-coverage: inherited def transform_entry(self, entry: Entry, library: "Library") -> Entry: - seen_normalized_keys: Set[str] = set() - new_fields_dict: Dict[str, Field] = {} + seen_normalized_keys: set[str] = set() + new_fields_dict: dict[str, Field] = {} for field in entry.fields: normalized_key: str = field.key.lower() # if the normalized key is already present, apply "last one wins" @@ -48,7 +45,7 @@ def transform_entry(self, entry: Entry, library: "Library") -> Entry: field.key = normalized_key new_fields_dict[normalized_key] = field - new_fields: List[Field] = list(new_fields_dict.values()) + new_fields: list[Field] = list(new_fields_dict.values()) entry.fields = new_fields return entry diff --git a/bibtexparser/middlewares/latex_encoding.py b/bibtexparser/middlewares/latex_encoding.py index ed6d13b..d95d5ff 100644 --- a/bibtexparser/middlewares/latex_encoding.py +++ b/bibtexparser/middlewares/latex_encoding.py @@ -1,9 +1,6 @@ import abc import logging import re -from typing import List -from typing import Optional -from typing import Tuple import pylatexenc from pylatexenc.latex2text import LatexNodes2Text @@ -29,7 +26,7 @@ class _PyStringTransformerMiddleware(BlockMiddleware, abc.ABC): """Abstract utility class allowing to modify python-strings""" @abc.abstractmethod - def _transform_python_value_string(self, python_string: str) -> Tuple[str, str]: + def _transform_python_value_string(self, python_string: str) -> tuple[str, str]: """Called for every python (value, not key) string found on Entry and String blocks. Returns: @@ -39,7 +36,7 @@ def _transform_python_value_string(self, python_string: str) -> Tuple[str, str]: raise NotImplementedError("called abstract method") # docstr-coverage: inherited - def _transform_all_strings(self, list_of_strings: List[str], errors: List[str]) -> List[str]: + def _transform_all_strings(self, list_of_strings: list[str], errors: list[str]) -> list[str]: """Called for every python (value, not key) string found on Entry and String blocks""" res = [] for s in list_of_strings: @@ -92,9 +89,9 @@ class LatexEncodingMiddleware(_PyStringTransformerMiddleware): def __init__( self, - keep_math: Optional[bool] = None, - enclose_urls: Optional[bool] = None, - encoder: Optional[UnicodeToLatexEncoder] = None, + keep_math: bool | None = None, + enclose_urls: bool | None = None, + encoder: UnicodeToLatexEncoder | None = None, allow_inplace_modification: bool = True, ): super().__init__( @@ -150,7 +147,7 @@ def metadata_key(cls) -> str: return "latex_encoding" # docstr-coverage: inherited - def _transform_python_value_string(self, python_string: str) -> Tuple[str, str]: + def _transform_python_value_string(self, python_string: str) -> tuple[str, str]: try: return self._encoder.unicode_to_latex(python_string), "" except Exception as e: @@ -163,9 +160,9 @@ class LatexDecodingMiddleware(_PyStringTransformerMiddleware): def __init__( self, allow_inplace_modification: bool = True, - keep_braced_groups: Optional[bool] = None, - keep_math_mode: Optional[bool] = None, - decoder: Optional[LatexNodes2Text] = None, + keep_braced_groups: bool | None = None, + keep_math_mode: bool | None = None, + decoder: LatexNodes2Text | None = None, ): super().__init__( allow_inplace_modification=allow_inplace_modification, @@ -218,7 +215,7 @@ def metadata_key(cls) -> str: return "latex_decoding" # docstr-coverage: inherited - def _transform_python_value_string(self, python_string: str) -> Tuple[str, str]: + def _transform_python_value_string(self, python_string: str) -> tuple[str, str]: """Transforms a python string to a latex string Returns: diff --git a/bibtexparser/middlewares/middleware.py b/bibtexparser/middlewares/middleware.py index 7df059d..65a012a 100644 --- a/bibtexparser/middlewares/middleware.py +++ b/bibtexparser/middlewares/middleware.py @@ -1,8 +1,7 @@ import abc import logging +from collections.abc import Collection from copy import deepcopy -from typing import Collection -from typing import Union from bibtexparser.library import Library from bibtexparser.model import Block @@ -101,9 +100,7 @@ def transform(self, library: "Library") -> "Library": # mid-pipeline; they are exposed as `library.failed_blocks`. return Library(blocks=blocks, fail_on_duplicate_key=False) - def transform_block( - self, block: Block, library: "Library" - ) -> Union[Block, Collection[Block], None]: + def transform_block(self, block: Block, library: "Library") -> Block | Collection[Block] | None: """Transform a block. :param block: Block to transform. @@ -138,9 +135,7 @@ def transform_block( logger.warning(f"Unknown block type {type(block)}") return block - def transform_entry( - self, entry: Entry, library: "Library" - ) -> Union[Block, Collection[Block], None]: + def transform_entry(self, entry: Entry, library: "Library") -> Block | Collection[Block] | None: """Transform an entry. Called by `transform_block` if the block is an entry. Note: This method modifies the passed entry. For a method @@ -151,7 +146,7 @@ def transform_entry( def transform_string( self, string: String, library: "Library" - ) -> Union[Block, Collection[Block], None]: + ) -> Block | Collection[Block] | None: """Transform a string. Called by `transform_block` if the block is a string. Note: This method modifies the passed string. For a method @@ -162,7 +157,7 @@ def transform_string( def transform_preamble( self, preamble: Preamble, library: "Library" - ) -> Union[Block, Collection[Block], None]: + ) -> Block | Collection[Block] | None: """Transform a preamble. Called by `transform_block` if the block is a preamble. Note: This method modifies the passed preamble. For a method @@ -173,7 +168,7 @@ def transform_preamble( def transform_explicit_comment( self, explicit_comment: ExplicitComment, library: "Library" - ) -> Union[Block, Collection[Block], None]: + ) -> Block | Collection[Block] | None: """Transform an explicit comment. Called by `transform_block` if the block is an explicit comment. Note: This method modifies the passed explicit comment. For a method @@ -184,7 +179,7 @@ def transform_explicit_comment( def transform_implicit_comment( self, implicit_comment: ImplicitComment, library: "Library" - ) -> Union[Block, Collection[Block], None]: + ) -> Block | Collection[Block] | None: """Transform an implicit comment. Called by `transform_block` if the block is an implicit comment. Note: This method modifies the passed implicit comment. For a method @@ -195,7 +190,7 @@ def transform_implicit_comment( def transform_failed_block( self, failed_block: ParsingFailedBlock, library: "Library" - ) -> Union[Block, Collection[Block], None]: + ) -> Block | Collection[Block] | None: """Transform a block whose parsing failed (e.g. a ``DuplicateFieldKeyBlock``). Called by `transform_block` if the block is a ``ParsingFailedBlock``. diff --git a/bibtexparser/middlewares/month.py b/bibtexparser/middlewares/month.py index 69dea39..0bbf0ed 100644 --- a/bibtexparser/middlewares/month.py +++ b/bibtexparser/middlewares/month.py @@ -1,8 +1,5 @@ import abc from collections import OrderedDict -from typing import Optional -from typing import Tuple -from typing import Union from bibtexparser.library import Library from bibtexparser.model import Block @@ -38,7 +35,7 @@ def transform_entry(self, entry: Entry, library: "Library") -> Block: return entry @abc.abstractmethod - def resolve_month_field_val(self, month_field: Field) -> Tuple[Union[str, int], str]: + def resolve_month_field_val(self, month_field: Field) -> tuple[str | int, str]: """Transform the month field. Args: @@ -48,7 +45,7 @@ def resolve_month_field_val(self, month_field: Field) -> Tuple[Union[str, int], A tuple of the transformed value and the metadata.""" raise NotImplementedError("Abstract method") - def _demanded_enclosing(self, new_value: Union[str, int]) -> Optional[str]: + def _demanded_enclosing(self, new_value: str | int) -> str | None: """The `Field.enclosing` to demand for the transformed month value, if any.""" return None @@ -141,7 +138,7 @@ def metadata_key(cls) -> str: return "MonthAbbreviationMiddleware" # docstr-coverage: inherited - def _demanded_enclosing(self, new_value: Union[str, int]) -> Optional[str]: + def _demanded_enclosing(self, new_value: str | int) -> str | None: # Month macros (jan, feb, ...) must not be enclosed when written, # as that would turn them into plain strings instead of references. if isinstance(new_value, str) and new_value in _MONTH_ABBREV: @@ -186,7 +183,7 @@ def metadata_key(cls) -> str: return "MonthIntMiddleware" # docstr-coverage: inherited - def _demanded_enclosing(self, new_value: Union[str, int]) -> Optional[str]: + def _demanded_enclosing(self, new_value: str | int) -> str | None: if isinstance(new_value, int): return "no-enclosing" return None diff --git a/bibtexparser/middlewares/names.py b/bibtexparser/middlewares/names.py index 45d1eb1..45713a9 100644 --- a/bibtexparser/middlewares/names.py +++ b/bibtexparser/middlewares/names.py @@ -6,10 +6,7 @@ import abc import dataclasses -from typing import List from typing import Literal -from typing import Tuple -from typing import Union from bibtexparser.library import Library from bibtexparser.model import Block @@ -37,7 +34,7 @@ class _NameTransformerMiddleware(BlockMiddleware, abc.ABC): def __init__( self, allow_inplace_modification: bool = True, - name_fields: Tuple[str, ...] = ("author", "editor", "translator"), + name_fields: tuple[str, ...] = ("author", "editor", "translator"), ): super().__init__( allow_inplace_modification=allow_inplace_modification, @@ -46,7 +43,7 @@ def __init__( self._name_fields = name_fields @property - def name_fields(self) -> Tuple[str, ...]: + def name_fields(self) -> tuple[str, ...]: """The fields that contain names, considered by this middleware.""" return self._name_fields @@ -75,7 +72,7 @@ def metadata_key(cls) -> str: return "separate_coauthors" # docstr-coverage: inherited - def _transform_field_value(self, name: str) -> List[str]: + def _transform_field_value(self, name: str) -> list[str]: return split_multiple_persons_names(name) @@ -88,7 +85,7 @@ def metadata_key(cls) -> str: return "merge_coauthors" # docstr-coverage: inherited - def _transform_field_value(self, name: Union[List[str], str]) -> str: + def _transform_field_value(self, name: list[str] | str) -> str: if isinstance(name, list): return " and ".join(name) return name @@ -101,10 +98,10 @@ class NameParts: The different parts are defined according to BibTex's implementation of name parts (first, von, last, jr).""" - first: List[str] = dataclasses.field(default_factory=list) - von: List[str] = dataclasses.field(default_factory=list) - last: List[str] = dataclasses.field(default_factory=list) - jr: List[str] = dataclasses.field(default_factory=list) + first: list[str] = dataclasses.field(default_factory=list) + von: list[str] = dataclasses.field(default_factory=list) + last: list[str] = dataclasses.field(default_factory=list) + jr: list[str] = dataclasses.field(default_factory=list) @property def merge_first_name_first(self) -> str: @@ -162,7 +159,7 @@ class SplitNameParts(_NameTransformerMiddleware): def metadata_key(cls) -> str: return "split_name_parts" - def _transform_field_value(self, name: List[str]) -> List[NameParts]: + def _transform_field_value(self, name: list[str]) -> list[NameParts]: if not isinstance(name, list): raise ValueError( "Expected a list of strings, got {}. " @@ -187,7 +184,7 @@ def __init__( self, style: Literal["last", "first"] = "last", allow_inplace_modification: bool = True, - name_fields: Tuple[str, ...] = ("author", "editor", "translator"), + name_fields: tuple[str, ...] = ("author", "editor", "translator"), ): self.style = style super().__init__(allow_inplace_modification, name_fields) @@ -197,7 +194,7 @@ def __init__( def metadata_key(cls) -> str: return "merge_name_parts" - def _transform_field_value(self, name: List[NameParts]) -> List[str]: + def _transform_field_value(self, name: list[NameParts]) -> list[str]: if not (isinstance(name, list) and all(isinstance(n, NameParts) for n in name)): raise ValueError(f"Expected a list of NameParts, got {name}. ") @@ -504,7 +501,7 @@ def rindex(k, x, default): return parts -def split_multiple_persons_names(names: str) -> List[str]: +def split_multiple_persons_names(names: str) -> list[str]: """ Splits a string of multiple names. diff --git a/bibtexparser/middlewares/parsestack.py b/bibtexparser/middlewares/parsestack.py index 227fccc..8bbe789 100644 --- a/bibtexparser/middlewares/parsestack.py +++ b/bibtexparser/middlewares/parsestack.py @@ -1,5 +1,3 @@ -from typing import List - from bibtexparser.middlewares import ResolveStringReferencesMiddleware from bibtexparser.middlewares.enclosing import AddEnclosingMiddleware from bibtexparser.middlewares.enclosing import RemoveEnclosingMiddleware @@ -7,7 +5,7 @@ from .middleware import Middleware -def default_parse_stack(allow_inplace_modification: bool = True) -> List[Middleware]: +def default_parse_stack(allow_inplace_modification: bool = True) -> list[Middleware]: """The default parse stack to be applied after splitting, if not specified otherwise.""" return [ ResolveStringReferencesMiddleware(allow_inplace_modification=allow_inplace_modification), @@ -15,7 +13,7 @@ def default_parse_stack(allow_inplace_modification: bool = True) -> List[Middlew ] -def default_unparse_stack(allow_inplace_modification: bool = False) -> List[Middleware]: +def default_unparse_stack(allow_inplace_modification: bool = False) -> list[Middleware]: """The default unparse stack to be applied before writing, if not specified otherwise.""" return [ AddEnclosingMiddleware( diff --git a/bibtexparser/middlewares/sorting_blocks.py b/bibtexparser/middlewares/sorting_blocks.py index a5300a7..956ebb6 100644 --- a/bibtexparser/middlewares/sorting_blocks.py +++ b/bibtexparser/middlewares/sorting_blocks.py @@ -1,11 +1,8 @@ +from collections.abc import Callable from copy import deepcopy from dataclasses import dataclass from dataclasses import field from typing import Any -from typing import Callable -from typing import List -from typing import Tuple -from typing import Type from bibtexparser.library import Library from bibtexparser.model import Block @@ -25,7 +22,7 @@ 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) + blocks: list[Block] = field(default_factory=list) @property def main_block(self) -> Block: @@ -104,7 +101,7 @@ def __init__( super().__init__(allow_inplace_modification=False) @staticmethod - def _block_chunks(blocks: List[Block]) -> List[_BlockChunk]: + def _block_chunks(blocks: list[Block]) -> list[_BlockChunk]: block_chunks = [] current_chunk = _BlockChunk() for block in blocks: @@ -141,7 +138,7 @@ class SortBlocksByTypeAndKeyMiddleware(SortBlocksMiddleware): def __init__( self, - block_type_order: Tuple[Type[Block], ...] = DEFAULT_BLOCK_TYPE_ORDER, + 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) @@ -160,7 +157,7 @@ def _verify_all_types_are_block_types(sort_order): "Sort order must only contain Block subclasses, " f"but got {str(t)}" ) - def _type_and_key_sort_key(self, block: Block) -> Tuple[int, str]: + 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)) diff --git a/bibtexparser/middlewares/sorting_entry_fields.py b/bibtexparser/middlewares/sorting_entry_fields.py index 5f19ffa..5424d6c 100644 --- a/bibtexparser/middlewares/sorting_entry_fields.py +++ b/bibtexparser/middlewares/sorting_entry_fields.py @@ -1,5 +1,3 @@ -from typing import Tuple - from bibtexparser.library import Library from bibtexparser.model import Block from bibtexparser.model import Entry @@ -35,7 +33,7 @@ class SortFieldsCustomMiddleware(BlockMiddleware): def __init__( self, - order: Tuple[str, ...], + order: tuple[str, ...], case_sensitive: bool = False, allow_inplace_modification: bool = True, ): diff --git a/bibtexparser/model.py b/bibtexparser/model.py index c7b807e..29837b8 100644 --- a/bibtexparser/model.py +++ b/bibtexparser/model.py @@ -1,15 +1,10 @@ import abc from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple _ALLOWED_ENCLOSINGS = (None, "{", '"', "no-enclosing") -def _validated_enclosing(enclosing: Optional[str]) -> Optional[str]: +def _validated_enclosing(enclosing: str | None) -> str | None: if enclosing not in _ALLOWED_ENCLOSINGS: raise ValueError( "enclosing must be one of None, '{', '\"' or 'no-enclosing', " f"not {enclosing!r}" @@ -25,23 +20,23 @@ class Block(abc.ABC): def __init__( self, - start_line: Optional[int] = None, - raw: Optional[str] = None, - parser_metadata: Optional[Dict[str, Any]] = None, + start_line: int | None = None, + raw: str | None = None, + parser_metadata: dict[str, Any] | None = None, ): self._start_line_in_file = start_line self._raw = raw if parser_metadata is None: parser_metadata = {} - self._parser_metadata: Dict[str, Any] = parser_metadata + self._parser_metadata: dict[str, Any] = parser_metadata @property - def start_line(self) -> Optional[int]: + def start_line(self) -> int | None: """The line number of the first line of this block in the parsed string.""" return self._start_line_in_file @property - def raw(self) -> Optional[str]: + def raw(self) -> str | None: """The raw, unmodified string (bibtex) representation of this block. Note: Middleware does not update this field, hence, after applying middleware @@ -50,7 +45,7 @@ def raw(self) -> Optional[str]: return self._raw @property - def parser_metadata(self) -> Dict[str, Any]: + def parser_metadata(self) -> dict[str, Any]: """EXPERIMENTAL: field for middleware to store auxiliary information. As an end-user, as long as you are not writing middleware, you probably @@ -66,7 +61,7 @@ def parser_metadata(self) -> Dict[str, Any]: """ return self._parser_metadata - def get_parser_metadata(self, key: str) -> Optional[Any]: + def get_parser_metadata(self, key: str) -> Any | None: """EXPERIMENTAL: get auxiliary information stored in ``parser_metadata``. See attribute ``parser_metadata`` for more information.""" @@ -100,9 +95,9 @@ def __init__( self, key: str, value: str, - start_line: Optional[int] = None, - raw: Optional[str] = None, - enclosing: Optional[str] = None, + start_line: int | None = None, + raw: str | None = None, + enclosing: str | None = None, ): super().__init__(start_line, raw) self._key = key @@ -129,7 +124,7 @@ def value(self, value: str): self._enclosing = None @property - def enclosing(self) -> Optional[str]: + def enclosing(self) -> str | None: """The enclosing demanded when writing this string, e.g. ``'{'``. Allowed values are ``'{'``, ``'"'`` and ``'no-enclosing'``. @@ -140,7 +135,7 @@ def enclosing(self) -> Optional[str]: return self._enclosing @enclosing.setter - def enclosing(self, enclosing: Optional[str]): + def enclosing(self, enclosing: str | None): self._enclosing = _validated_enclosing(enclosing) def __str__(self) -> str: @@ -156,7 +151,7 @@ def __repr__(self) -> str: class Preamble(Block): """Bibtex Blocks of the ``@preamble`` type, e.g. ``@preamble{This is a preamble}``.""" - def __init__(self, value: str, start_line: Optional[int] = None, raw: Optional[str] = None): + def __init__(self, value: str, start_line: int | None = None, raw: str | None = None): super().__init__(start_line, raw) self._value = value @@ -179,7 +174,7 @@ def __repr__(self) -> str: class ExplicitComment(Block): """Bibtex Blocks of the ``@comment`` type, e.g. ``@comment{This is a comment}``.""" - def __init__(self, comment: str, start_line: Optional[int] = None, raw: Optional[str] = None): + def __init__(self, comment: str, start_line: int | None = None, raw: str | None = None): super().__init__(start_line, raw) self._comment = comment @@ -205,7 +200,7 @@ def __repr__(self) -> str: class ImplicitComment(Block): """Bibtex outside of an ``@{...}`` block, which is treated as a comment.""" - def __init__(self, comment: str, start_line: Optional[int] = None, raw: Optional[str] = None): + def __init__(self, comment: str, start_line: int | None = None, raw: str | None = None): super().__init__(start_line, raw) self._comment = comment @@ -235,8 +230,8 @@ def __init__( self, key: str, value: Any, - start_line: Optional[int] = None, - enclosing: Optional[str] = None, + start_line: int | None = None, + enclosing: str | None = None, ): self._start_line = start_line self._key = key @@ -263,7 +258,7 @@ def value(self, value: Any): self._enclosing = None @property - def enclosing(self) -> Optional[str]: + def enclosing(self) -> str | None: """The enclosing demanded when writing this field, e.g. ``'{'``. Allowed values are ``'{'``, ``'"'`` and ``'no-enclosing'``. @@ -279,7 +274,7 @@ def enclosing(self) -> Optional[str]: return self._enclosing @enclosing.setter - def enclosing(self, enclosing: Optional[str]): + def enclosing(self, enclosing: str | None): self._enclosing = _validated_enclosing(enclosing) @property @@ -318,9 +313,9 @@ def __init__( self, entry_type: str, key: str, - fields: List[Field], - start_line: Optional[int] = None, - raw: Optional[str] = None, + fields: list[Field], + start_line: int | None = None, + raw: str | None = None, ): super().__init__(start_line, raw) self._entry_type = entry_type @@ -346,16 +341,16 @@ def key(self, value: str): self._key = value @property - def fields(self) -> List[Field]: + def fields(self) -> list[Field]: """The key-value attributes of an entry, as ``Field`` instances.""" return self._fields @fields.setter - def fields(self, value: List[Field]): + def fields(self, value: list[Field]): self._fields = value @property - def fields_dict(self) -> Dict[str, Field]: + def fields_dict(self) -> dict[str, Field]: """A dict of fields, with field keys as keys. Note that with duplicate field keys, the behavior is undefined.""" @@ -369,7 +364,7 @@ def set_field(self, field: Field): else: self._fields.append(field) - def pop(self, key: str, default=None) -> Optional[Field]: + def pop(self, key: str, default=None) -> Field | None: """Removes and returns the field with the given key. :param key: The key of the field to remove. @@ -382,7 +377,7 @@ def pop(self, key: str, default=None) -> Optional[Field]: self._fields = [f for f in self._fields if f.key != key] return field - def get(self, key: str, default=None) -> Optional[Field]: + def get(self, key: str, default=None) -> Field | None: """Returns the field with the given key, or the default value if it does not exist. :param key: The key of the field. @@ -436,7 +431,7 @@ def __delitem__(self, key: str) -> None: """ self.pop(key) - def items(self) -> List[Tuple[str, Any]]: + def items(self) -> list[tuple[str, Any]]: """Dict-mimicking, for partial v1.x backwards compatibility. For newly written code, it's recommended to use `entry.entry_type`, @@ -464,9 +459,9 @@ class ParsingFailedBlock(Block): def __init__( self, error: Exception, - start_line: Optional[int] = None, - raw: Optional[str] = None, - ignore_error_block: Optional[Block] = None, + start_line: int | None = None, + raw: str | None = None, + ignore_error_block: Block | None = None, ): super().__init__(start_line, raw) self._error = error @@ -478,7 +473,7 @@ def error(self) -> Exception: return self._error @property - def ignore_error_block(self) -> Optional[Block]: + def ignore_error_block(self) -> Block | None: """The possibly faulty block when ignoring the error. This may be None, as it may not always be possible to ignore the error. @@ -512,8 +507,8 @@ def __init__( key: str, previous_block: Block, duplicate_block: Block, - start_line: Optional[int] = None, - raw: Optional[str] = None, + start_line: int | None = None, + raw: str | None = None, ): super().__init__( error=Exception(f"Duplicate entry key '{key}'"), @@ -545,7 +540,7 @@ class DuplicateFieldKeyBlock(ParsingFailedBlock): The entry containing the duplicate field keys is available as `block.ignore_error_block`, the duplicate keys as `block.duplicate_keys`.""" - def __init__(self, duplicate_keys: Set[str], entry: Entry): + def __init__(self, duplicate_keys: set[str], entry: Entry): sorted_duplicate_keys = sorted(list(duplicate_keys)) super().__init__( error=Exception( @@ -557,9 +552,9 @@ def __init__(self, duplicate_keys: Set[str], entry: Entry): raw=entry.raw, ignore_error_block=entry, ) - self._duplicate_keys: Set[str] = duplicate_keys + self._duplicate_keys: set[str] = duplicate_keys @property - def duplicate_keys(self) -> Set[str]: + def duplicate_keys(self) -> set[str]: """The field-keys that occurred more than once in the entry.""" return self._duplicate_keys diff --git a/bibtexparser/splitter.py b/bibtexparser/splitter.py index b59c7c0..bbc3e97 100644 --- a/bibtexparser/splitter.py +++ b/bibtexparser/splitter.py @@ -1,10 +1,5 @@ import logging import re -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple -from typing import Union from .exceptions import BlockAbortedException from .exceptions import ParserStateException @@ -51,7 +46,7 @@ def _reset_block_status(self, current_char_index: int) -> None: # at the beginning of the file and after each @{...} block. # We then ignore empty implicit comments. self._implicit_comment_start_line = self._current_line - self._implicit_comment_start: Optional[int] = current_char_index + self._implicit_comment_start: int | None = current_char_index def _is_at_line_start(self, pos: int) -> bool: """Check if position is at the start of a line (after optional whitespace). @@ -71,7 +66,7 @@ def _is_at_line_start(self, pos: int) -> bool: # Start of string counts as line start return True - def _end_implicit_comment(self, end_char_index) -> Optional[ImplicitComment]: + def _end_implicit_comment(self, end_char_index) -> ImplicitComment | None: if self._implicit_comment_start is None: return # No implicit comment started @@ -98,7 +93,7 @@ def _end_implicit_comment(self, end_char_index) -> Optional[ImplicitComment]: else: return None - def _next_mark(self, accept_eof: bool) -> Optional[re.Match]: + def _next_mark(self, accept_eof: bool) -> re.Match | None: # Check if there is a mark that was previously not consumed # and return it if so if self._unaccepted_mark is not None: @@ -225,7 +220,7 @@ def _is_escaped(): end_index=next_mark.start() - 1, ) - def _move_to_end_of_entry(self, first_key_start: int) -> Tuple[List[Field], int, Set[str]]: + def _move_to_end_of_entry(self, first_key_start: int) -> tuple[list[Field], int, set[str]]: """Move to the end of the entry and return the fields and the end index.""" result = [] keys = set() @@ -280,7 +275,7 @@ def _move_to_end_of_entry(self, first_key_start: int) -> Tuple[List[Field], int, end_index=after_field_mark.start(), ) - def split(self, library: Optional[Library] = None) -> Library: + def split(self, library: Library | None = None) -> Library: """Split the bibtex-string into blocks and add them to the library. Args: @@ -390,7 +385,7 @@ def _handle_explicit_comment(self) -> ExplicitComment: raw=self.bibstr[start_index : end_bracket_index + 1], ) - def _handle_entry(self, m, m_val) -> Union[Entry, ParsingFailedBlock]: + def _handle_entry(self, m, m_val) -> Entry | ParsingFailedBlock: """Handle entry block. Return end index""" start_line = self._current_line entry_type = m_val[1:].strip() diff --git a/bibtexparser/writer.py b/bibtexparser/writer.py index 932671c..421b274 100644 --- a/bibtexparser/writer.py +++ b/bibtexparser/writer.py @@ -1,7 +1,5 @@ from copy import deepcopy -from typing import List from typing import Optional -from typing import Union from .library import Library from .model import Entry @@ -16,7 +14,7 @@ PARSING_FAILED_COMMENT = "% WARNING Parsing failed for the following {n} lines." -def _treat_entry(block: Entry, bibtex_format) -> List[str]: +def _treat_entry(block: Entry, bibtex_format) -> list[str]: res = ["@", block.entry_type, "{", block.key, ",\n"] field: Field for i, field in enumerate(block.fields): @@ -38,7 +36,7 @@ def _val_indent_string(bibtex_format: "BibtexFormat", key: str) -> str: return "" if length <= 0 else " " * length -def _treat_string(block: String, bibtex_format) -> List[str]: +def _treat_string(block: String, bibtex_format) -> list[str]: return [ "@string{", block.key, @@ -49,20 +47,20 @@ def _treat_string(block: String, bibtex_format) -> List[str]: ] -def _treat_preamble(block: Preamble, bibtex_format: "BibtexFormat") -> List[str]: +def _treat_preamble(block: Preamble, bibtex_format: "BibtexFormat") -> list[str]: return [f"@preamble{{{block.value}}}\n"] -def _treat_impl_comment(block: ImplicitComment, bibtex_format: "BibtexFormat") -> List[str]: +def _treat_impl_comment(block: ImplicitComment, bibtex_format: "BibtexFormat") -> list[str]: # Note: No explicit escaping is done here - that should be done in middleware return [block.comment, "\n"] -def _treat_expl_comment(block: ExplicitComment, bibtex_format: "BibtexFormat") -> List[str]: +def _treat_expl_comment(block: ExplicitComment, bibtex_format: "BibtexFormat") -> list[str]: return ["@comment{", block.comment, "}\n"] -def _treat_failed_block(block: ParsingFailedBlock, bibtex_format: "BibtexFormat") -> List[str]: +def _treat_failed_block(block: ParsingFailedBlock, bibtex_format: "BibtexFormat") -> list[str]: if block.raw is None: raise ValueError(_failed_blocks_without_raw_error([block])) lines = len(block.raw.splitlines()) @@ -70,7 +68,7 @@ def _treat_failed_block(block: ParsingFailedBlock, bibtex_format: "BibtexFormat" return [parsing_failed_comment, "\n", block.raw, "\n"] -def _failed_blocks_without_raw_error(blocks: List[ParsingFailedBlock]) -> str: +def _failed_blocks_without_raw_error(blocks: list[ParsingFailedBlock]) -> str: descriptions = "\n".join(f" - {type(b).__name__}: {b.error}" for b in blocks) return ( "Cannot write library: it contains failed blocks without raw bibtex " @@ -132,7 +130,7 @@ def write(library: Library, bibtex_format: Optional["BibtexFormat"] = None) -> s return "".join(string_pieces) -def _treat_block(bibtex_format, block) -> List[str]: +def _treat_block(bibtex_format, block) -> list[str]: if isinstance(block, Entry): string_block_pieces = _treat_entry(block, bibtex_format) elif isinstance(block, String): @@ -159,7 +157,7 @@ class BibtexFormat: def __init__(self): self._indent: str = "\t" - self._align_field_values: Union[int, str] = 0 + self._align_field_values: int | str = 0 self._block_separator: str = "\n\n" self._trailing_comma: bool = False self._parsing_failed_comment: str = PARSING_FAILED_COMMENT @@ -174,7 +172,7 @@ def indent(self, indent: str): self._indent = indent @property - def value_column(self) -> Union[int, str]: + def value_column(self) -> int | str: """Controls the alignment of field- and string-values. Default: no alignment. This impacts String and Entry blocks. @@ -194,7 +192,7 @@ def value_column(self) -> Union[int, str]: return self._align_field_values @value_column.setter - def value_column(self, align_values: Union[int, str]): + def value_column(self, align_values: int | str): if isinstance(align_values, int): if align_values < 0: raise ValueError("align_field_values must be >= 0") diff --git a/tests/e2e_example.py b/tests/e2e_example.py index 4ebea30..8236ff0 100644 --- a/tests/e2e_example.py +++ b/tests/e2e_example.py @@ -6,8 +6,7 @@ from bibtexparser.middlewares.names import SeparateCoAuthors from bibtexparser.middlewares.names import SplitNameParts -bibtex_string = dedent( - """\ +bibtex_string = dedent("""\ @article{Muller2020, title = "Some Paper Title", author = "John Muller and Jane Doe", @@ -19,8 +18,7 @@ This is a comment. } -@preamble{e = mc^2} """ -) +@preamble{e = mc^2} """) def test_example(): @@ -42,10 +40,7 @@ def test_example(): ) # Note: As defaults change, this assertion may need to be updated. - assert ( - new_bibtex_string.strip() - == dedent( - """ + assert new_bibtex_string.strip() == dedent(""" @article{Muller2020, \ttitle = {Some Paper Title}, \tauthor = {John Muller and Jane Doe}, @@ -57,6 +52,4 @@ def test_example(): @comment{This is a comment.} - @preamble{e = mc^2}""" - ).strip() - ) + @preamble{e = mc^2}""").strip() diff --git a/tests/middleware_tests/middleware_test_util.py b/tests/middleware_tests/middleware_test_util.py index 1d22b2a..15892a7 100644 --- a/tests/middleware_tests/middleware_test_util.py +++ b/tests/middleware_tests/middleware_test_util.py @@ -1,5 +1,4 @@ from copy import deepcopy -from typing import Optional from bibtexparser.library import Library from bibtexparser.middlewares.middleware import Middleware @@ -10,7 +9,7 @@ def assert_block_does_not_change( - block_type: str, middleware: Middleware, same_instance: Optional[bool] + block_type: str, middleware: Middleware, same_instance: bool | None ): """Utility to make sure blocks of some types are not changed by middleware.""" block_type = block_type.lower() diff --git a/tests/middleware_tests/test_enclosing.py b/tests/middleware_tests/test_enclosing.py index 7964eb3..e5e021f 100644 --- a/tests/middleware_tests/test_enclosing.py +++ b/tests/middleware_tests/test_enclosing.py @@ -1,5 +1,4 @@ from copy import deepcopy -from typing import Union import pytest @@ -126,7 +125,7 @@ def test_addition_of_enclosing_on_entry( default_enclosing: str, enclose_ints: bool, reuse_previous_enclosing: bool, - value: Union[str, int], + value: str | int, inplace: bool, ): """Extensive Matrix-Testing of the AddEnclosingMiddleware on Entries. diff --git a/tests/middleware_tests/test_latex_encoding.py b/tests/middleware_tests/test_latex_encoding.py index f8057b4..64c3d88 100644 --- a/tests/middleware_tests/test_latex_encoding.py +++ b/tests/middleware_tests/test_latex_encoding.py @@ -4,7 +4,6 @@ Thus, we merely test that the middleware is correctly configured.""" from copy import deepcopy -from typing import Tuple import pytest @@ -146,7 +145,7 @@ def metadata_key(cls) -> str: return "failing_test_middleware" # docstr-coverage: inherited - def _transform_python_value_string(self, python_string: str) -> Tuple[str, str]: + def _transform_python_value_string(self, python_string: str) -> tuple[str, str]: return python_string, "some error" library = Library([String(key="me", value="some value", start_line=1, raw="irrelevant")]) diff --git a/tests/middleware_tests/test_names.py b/tests/middleware_tests/test_names.py index fc99d50..e11133b 100644 --- a/tests/middleware_tests/test_names.py +++ b/tests/middleware_tests/test_names.py @@ -1,6 +1,4 @@ from copy import deepcopy -from typing import Dict -from typing import List import pytest as pytest @@ -83,7 +81,7 @@ ("~~~ and J. Smith", ["~~~", "J. Smith"]), ], ) -def test_split_coauthors_consistent_with_bibtex(field_value: str, expected: List[str]): +def test_split_coauthors_consistent_with_bibtex(field_value: str, expected: list[str]): """Tests the utility function which splits a string of coauthors into a list of names. The test cases are taken from https://github.com/sciunto-org/python-bibtexparser/pull/140/files @@ -203,7 +201,7 @@ def _dict_to_nameparts(as_dict): ), ), ) -def test_name_splitting_no_strict_mode(name: str, expected: Dict[str, List[str]]): +def test_name_splitting_no_strict_mode(name: str, expected: dict[str, list[str]]): result = parse_single_name_into_parts(name, strict=False) expected = _dict_to_nameparts(expected) assert result == expected @@ -883,7 +881,7 @@ def test_merge_last_name_first_inverse(name, expected_as_dict, strict): # cases where either the last name or "von" part ends with an odd number of `\` cannot be handled, # since in those cases the `,` is escaped when the name parts are put back together - def ends_with_odd_slash(names: List[str]) -> bool: + def ends_with_odd_slash(names: list[str]) -> bool: if len(names) == 0: return False name = names[-1] @@ -1027,7 +1025,7 @@ def test_split_name_parts(inplace: bool): ), ], ) -def test_merge_name_parts(inplace: bool, style: str, names: List[str]): +def test_merge_name_parts(inplace: bool, style: str, names: list[str]): input_entry = Entry( start_line=0, raw="irrelevant-for-this-test", diff --git a/tests/resources.py b/tests/resources.py index e064695..dbbca49 100644 --- a/tests/resources.py +++ b/tests/resources.py @@ -1,9 +1,8 @@ from textwrap import dedent -from typing import List import pytest -EDGE_CASE_VALUES: List[str] = [ +EDGE_CASE_VALUES: list[str] = [ "John Doe", r"à {\`a} \`{a}", r"{\`a} {\`a} {\`a}", @@ -16,29 +15,25 @@ r"Title with { UnEscaped Curly } Braces", ] -ENCLOSINGS: List[pytest.param] = [ +ENCLOSINGS: list[pytest.param] = [ pytest.param('"{0}"', id="double_quotes"), pytest.param("{{{0}}}", id="curly_braces"), ] -VALID_BIBTEX_SNIPPETS: List[str] = [ +VALID_BIBTEX_SNIPPETS: list[str] = [ # A small, regular article - dedent( - """\ + dedent("""\ @article{test, author = "John Doe", title = "Some title", - }""" - ), + }"""), # A string definition dedent("""@string{someString = "some value"}"""), # A string definition with a comment - dedent( - """\ + dedent("""\ @string{someString = "some value"} - % This is a comment""" - ), + % This is a comment"""), # A preamble dedent("""@preamble{some preamble}"""), # A an empty line diff --git a/tests/splitter_tests/test_splitter_basic.py b/tests/splitter_tests/test_splitter_basic.py index 0a7d171..58be5c4 100644 --- a/tests/splitter_tests/test_splitter_basic.py +++ b/tests/splitter_tests/test_splitter_basic.py @@ -4,7 +4,6 @@ More exhaustive and atomic tests are provided in separate modules.""" from typing import Any -from typing import Dict import pytest as pytest @@ -135,7 +134,7 @@ def _split() -> Library: }, ], ) -def test_entry(expected: Dict[str, Any]): +def test_entry(expected: dict[str, Any]): library = _split() assert len(library.entries) == 2 entry_by_key = library.entries_dict @@ -177,7 +176,7 @@ def test_entry(expected: Dict[str, Any]): }, ], ) -def test_strings(expected: Dict[str, any]) -> None: +def test_strings(expected: dict[str, any]) -> None: library = _split() assert len(library.strings) == 2 # Raise KeyError if not found @@ -235,7 +234,7 @@ def test_preamble(): }, ], ) -def test_comments(expected: Dict[str, any]) -> None: +def test_comments(expected: dict[str, any]) -> None: library = _split() comments = library.comments assert len(comments) == 3 @@ -376,7 +375,7 @@ def test_handles_duplicate_strings(): }, ], ) -def test_blocks_not_starting_on_new_lines(expected: Dict[str, Any]): +def test_blocks_not_starting_on_new_lines(expected: dict[str, Any]): """Test the new blocks that are not on new lines. Discussed at https://github.com/sciunto-org/python-bibtexparser/issues/411. diff --git a/tests/splitter_tests/test_splitter_block_start_detection.py b/tests/splitter_tests/test_splitter_block_start_detection.py index b50ef09..3d7553e 100644 --- a/tests/splitter_tests/test_splitter_block_start_detection.py +++ b/tests/splitter_tests/test_splitter_block_start_detection.py @@ -21,14 +21,12 @@ "bibtex_str,expected_key,expected_field,expected_substring", [ pytest.param( - dedent( - """\ + dedent("""\ @inproceedings{DBLP:conf/cikm/EsuliM021, author = {Andrea Esuli}, title = {LeQua @ {CLEF} 2022: {A} Shared Task}, year = {2021} - }""" - ), + }"""), "DBLP:conf/cikm/EsuliM021", "title", "@ {CLEF}", @@ -165,29 +163,23 @@ def test_mixed_blocks_same_line( "bibtex_str,expected_valid_key", [ pytest.param( - dedent( - """\ + dedent("""\ @article{broken, title={Unclosed - @article{valid, title={Valid Entry}}""" - ), + @article{valid, title={Valid Entry}}"""), "valid", id="unclosed_entry_field", ), pytest.param( - dedent( - """\ + dedent("""\ @string{broken = {unclosed value - @article{valid, title={Valid Entry}}""" - ), + @article{valid, title={Valid Entry}}"""), "valid", id="unclosed_string", ), pytest.param( - dedent( - """\ + dedent("""\ @article{broken, title={Unclosed - @article{valid, title={Valid Entry}}""" - ), + @article{valid, title={Valid Entry}}"""), "valid", id="indented_new_block", ), @@ -204,11 +196,9 @@ def test_error_recovery_at_line_start(bibtex_str: str, expected_valid_key: str): def test_error_recovery_preserves_failed_block_raw(): """The failed block should contain raw text up to where recovery started.""" - bibtex_str = dedent( - """\ + bibtex_str = dedent("""\ @article{broken, title={This is unclosed - @article{valid, title={OK}}""" - ) + @article{valid, title={OK}}""") library = Splitter(bibtex_str).split() assert len(library.failed_blocks) == 1 diff --git a/tests/splitter_tests/test_splitter_entry.py b/tests/splitter_tests/test_splitter_entry.py index 6479afa..5d57da0 100644 --- a/tests/splitter_tests/test_splitter_entry.py +++ b/tests/splitter_tests/test_splitter_entry.py @@ -112,15 +112,13 @@ def test_field_value(field_value: str, enclosing: str): def test_trailing_comma(enclosing: str): """Test that a trailing comma is correctly parsed (i.e., ignored).""" value_before_trailing_comma = enclosing.format("valueBeforeTrailingComma") - bibtex_str = dedent( - f"""\ + bibtex_str = dedent(f"""\ @article{{test, firstfield = {{some value}}, fieldBeforeTrailingComma = {value_before_trailing_comma}, }} - @string{{someString = "some value"}}""" - ) + @string{{someString = "some value"}}""") library: Library = Splitter(bibtex_str).split() assert len(library.failed_blocks) == 0 assert len(library.entries) == 1 @@ -139,8 +137,7 @@ def test_trailing_comma(enclosing: str): def test_multiple_identical_field_keys(): - bibtex_str = dedent( - """\ + bibtex_str = dedent("""\ @article{test, title = {The first title}, author = {The first author}, @@ -148,8 +145,7 @@ def test_multiple_identical_field_keys(): title = {The third title}, author = {The second author}, journal = {Some journal} - }""" - ) + }""") library: Library = Splitter(bibtex_str).split() assert len(library.blocks) == 1 diff --git a/tests/splitter_tests/test_splitter_implicit_comments.py b/tests/splitter_tests/test_splitter_implicit_comments.py index 826e7b9..2168732 100644 --- a/tests/splitter_tests/test_splitter_implicit_comments.py +++ b/tests/splitter_tests/test_splitter_implicit_comments.py @@ -11,12 +11,10 @@ def test_implicit_comment_eof(): """Makes sure implicit comments at end of file are parsed.""" - bibtex_str = dedent( - """\ + bibtex_str = dedent("""\ @article{article1, title={title1}} - % This is an implicit comment at the end of the file.""" - ) + % This is an implicit comment at the end of the file.""") library = Splitter(bibtex_str).split() @@ -68,8 +66,7 @@ def test_implicit_comment_on_block_end_line(block): def test_multiline_implicit_comment(): """Makes sure implicit comments spanning multiple lines are parsed.""" - bibtex_str = dedent( - """\ + bibtex_str = dedent("""\ @article{article1, title={title1}} % This is an implicit comment @@ -77,20 +74,17 @@ def test_multiline_implicit_comment(): with an empty line in between. - @article{article2, title={title2}}""" - ) + @article{article2, title={title2}}""") library = Splitter(bibtex_str).split() assert len(library.comments) == 1 - expected_str = dedent( - """\ + expected_str = dedent("""\ % This is an implicit comment % spanning multiple lines - with an empty line in between.""" - ) + with an empty line in between.""") assert library.comments[0].comment == expected_str # Before applying the middleware, `comment` and `raw` are the same. assert library.comments[0].raw == expected_str diff --git a/tests/test_model.py b/tests/test_model.py index 0382a1c..d24f42b 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -329,12 +329,10 @@ def test_entry_str(): ], ) - expected = dedent( - """\ + expected = dedent("""\ Entry (line: None, type: `article`, key: `myEntry`): \t`myFirstField` = `firstValue` - \t`mySecondField` = `secondValue`""" - ) + \t`mySecondField` = `secondValue`""") assert str(entry) == expected