Source code for bibble.util.middlecore

  1#!/usr/bin/env python3
  2"""
  3Refactored core middleware classes from bibtexparser
  4
  5"""
  6# Imports:
  7from __future__ import annotations
  8
  9# ##-- stdlib imports
 10import datetime
 11import enum
 12import functools as ftz
 13import itertools as itz
 14import logging as logmod
 15import pathlib as pl
 16import re
 17import time
 18import types
 19import collections
 20import contextlib
 21import hashlib
 22from copy import deepcopy
 23from uuid import UUID, uuid1
 24from weakref import ref
 25import atexit # for @atexit.register
 26import faulthandler
 27import sys
 28# ##-- end stdlib imports
 29
 30import tqdm
 31import bibble._interface as API
 32from bibble.model import MetaBlock
 33import jgdv
 34from jgdv._abstract.protocols.general import DILogger_p
 35from jgdv import Proto
 36from bibtexparser.library import Library
 37from bibtexparser import model
 38
 39# ##-- types
 40# isort: off
 41import abc
 42import collections.abc
 43from typing import TYPE_CHECKING, cast, assert_type, assert_never
 44from typing import Generic, NewType
 45# Protocols:
 46from typing import Protocol, runtime_checkable
 47# Typing Decorators:
 48from typing import no_type_check, final, override, overload
 49
 50if TYPE_CHECKING:
 51    from jgdv import Maybe
 52    from typing import Final
 53    from typing import ClassVar, Any, LiteralString
 54    from typing import Never, Self, Literal
 55    from typing import TypeGuard
 56    from collections.abc import Iterable, Iterator, Callable, Generator
 57    from collections.abc import Sequence, Mapping, MutableMapping, Hashable
 58
 59    type Logger          = logmod.Logger
 60    type Block           = model.Block
 61    type Entry           = model.Entry
 62    type String          = model.String
 63    type Preamble        = model.Preamble
 64    type ExplicitComment = model.ExplicitComment
 65    type ImplicitComment = model.ImplicitComment
 66##--|
 67
 68# isort: on
 69# ##-- end types
 70
 71##-- logging
 72logging = logmod.getLogger(__name__)
 73##-- end logging
 74
 75# Vars:
 76
 77# Body:
 78
[docs] 79@Proto(DILogger_p) 80class _BaseMiddleware: 81 """ 82 The base middleware. 83 has a metadata key, 84 stores allow_inplace and allow_parallel 85 and can have an injected logger. 86 87 Any extra init kwargs are stored in _extra 88 """ 89 allow_inplace : bool 90 allow_parallel : bool 91
[docs] 92 @classmethod 93 def metadata_key(cls) -> str: 94 """Identifier of the middleware. 95 This key is used to identify the middleware in a blocks metadata. 96 """ 97 return f"bibble-{cls.__name__}"
98 99 def __init__(self, **kwargs): 100 """ 101 102 """ 103 inplace = kwargs.pop(API.ALLOW_INPLACE_MOD_K, True) 104 self.allow_inplace = inplace 105 self.allow_parallel = kwargs.pop(API.ALLOW_PARALLEL_K, False) 106 fallback_name = f"{type(self).__module__}.{type(self).__name__}" 107 self._logger = kwargs.pop(API.LOGGER_K, logmod.getLogger(fallback_name)) 108 self._extra = kwargs 109
[docs] 110 def logger(self) -> Logger: 111 return self._logger
112
[docs] 113 def _get_lib_iterator(self, library:Library) -> tuple[Library, Iterator[Block]]: 114 match self.allow_inplace: 115 case True: 116 library = library 117 case False: 118 library = deepcopy(library) 119 case x: 120 raise TypeError(type(x)) 121 122 match self._extra: 123 case {"tqdm":True} if sys.stdout.isatty(): 124 iterator = tqdm.tqdm(enumerate(library.blocks), 125 desc=type(self).__name__, 126 total=len(library.blocks), 127 ncols=API.TQDM_WIDTH) 128 case _: 129 iterator = enumerate(library.blocks) 130 131 return library, iterator
132
[docs] 133 def handle_meta_entry(self, library:Library) -> None: 134 """ An optional entry hook for middlewares, 135 which is given the library's metablock before transform is called. 136 137 Use case: conditionally setting PathWriter suppress paths 138 """ 139 pass
[docs] 140class IdenLibraryMiddleware(_BaseMiddleware): 141 """ Identity Library Middleware, does nothing """ 142
[docs] 143 def transform(self, library:Library) -> Library: 144 match library: 145 case Library() if self.allow_inplace: 146 return library 147 case Library(): 148 return deepcopy(library)
149
[docs] 150@Proto(API.AdaptiveMiddleware_p, API.Middleware_p) 151class IdenBlockMiddleware(_BaseMiddleware): 152 """ Identity Block Middleware, does nothing 153 If passed 'tqdm'=True uses tqdm around the block level loop 154 """ 155 _transform_cache : dict[type, list[Callable]] 156 157 def __init__(self, *args, **kwargs): 158 super().__init__(*args, **kwargs) 159 self._transform_cache = dict() 160
[docs] 161 def get_transforms_for(self, block:Block, *, direction:Maybe[str]=None) -> list[Callable]: 162 """ Get all transforms of the form transform_{Type}, 163 by mro, from most -> least specific 164 """ 165 assert(direction is None) 166 if type(block) not in self._transform_cache: 167 mro = type(block).mro() 168 formatted = [f"transform_{x.__name__}" for x in mro] 169 methods = [getattr(self, x, None) for x in formatted] 170 self._transform_cache[type(block)] = [x for x in methods if x is not None] 171 172 return self._transform_cache[type(block)]
173
[docs] 174 def transform(self, library:Library) -> Library: 175 library, iterator = self._get_lib_iterator(library) 176 blocks = [] 177 for i,block in iterator: 178 match self.get_transforms_for(block): 179 case [x, *_]: # Use the first found transform, 180 transform = x 181 case _: # No transforms for this block type, do nothing 182 blocks.append(block) 183 continue 184 185 match transform(block, library): 186 case None: # remove block 187 continue 188 case []: # Keep original (it might have been modified) 189 blocks.append(block) 190 case [*xs]: # new blocks 191 blocks += xs 192 case x: 193 raise TypeError(type(x), i, block) 194 195 else: 196 # Remove the old blocks 197 library.remove(library.blocks[:]) 198 # Add the new blocks 199 library.add(blocks) 200 return library
201
[docs] 202@Proto(API.AdaptiveMiddleware_p, API.BidirectionalMiddleware_p) 203class IdenBidiMiddleware(_BaseMiddleware): 204 205 _transform_cache : dict[str, list[Callable]] 206 _reader : Maybe[API.Middleware] 207 _writer : Maybe[API.Middleware] 208 209 def __init__(self, *args, **kwargs): 210 super().__init__(*args, **kwargs) 211 self._reader = None 212 self._writer = None 213 self._transform_cache = dict() 214 215
[docs] 216 def handle_meta_entry(self, library:Library) -> None: 217 match self._reader: 218 case None: 219 pass 220 case mw if not hasattr(mw, "handle_meta_entry"): 221 pass 222 case mw: 223 mw.handle_meta_entry(library) 224 225 match self._writer: 226 case None: 227 pass 228 case mw if not hasattr(mw, "handle_meta_entry"): 229 pass 230 case mw: 231 mw.handle_meta_entry(library)
232 233
[docs] 234 def get_transforms_for(self, block:Block, *, direction:Maybe[str]=None) -> list[Callable]: 235 """ Get all transforms of the form {direction}_transform_{Type}, 236 by mro, from most -> least specific 237 """ 238 cache_key = f"{direction}_{type(block).__name__}" 239 if cache_key not in self._transform_cache: 240 mro = type(block).mro() 241 formatted = [f"{direction}_transform_{x.__name__}" for x in mro] 242 methods = [getattr(self, x, None) for x in formatted] 243 self._transform_cache[cache_key] = [x for x in methods if x is not None] 244 245 return self._transform_cache[cache_key]
246 247
[docs] 248 def read_transform(self, library:Library) -> Library: 249 library, iterator = self._get_lib_iterator(library) 250 blocks = [] 251 for i,block in iterator: 252 match self.get_transforms_for(block, direction="read"): 253 case [x, *_]: # Use the first found transform, 254 transform = x 255 case _: 256 blocks.append(block) 257 continue 258 259 match transform(block, library): 260 case [] | None: # Transform gave nothing, so keep the original block. 261 blocks.append(block) 262 case [*xs]: 263 # new blocks 264 blocks += xs 265 case x: 266 raise TypeError(type(x), i, block) 267 else: 268 # Remove the old blocks 269 library.remove(library.blocks[:]) 270 assert(len(library.blocks) == 0), len(library.blocks) 271 # Add the new blocks 272 library.add(blocks) 273 return library
274
[docs] 275 def write_transform(self, library:Library) -> Library: 276 library, iterator = self._get_lib_iterator(library) 277 blocks = [] 278 for i,block in iterator: 279 match self.get_transforms_for(block, direction="write"): 280 case [x, *_]: # Use the first found transform, 281 transform = x 282 case _: 283 blocks.append(block) 284 continue 285 286 match transform(block, library): 287 case [] | None: # Transform gave nothing, so keep the original block. 288 blocks.append(block) 289 case [*xs]: # new blocks 290 blocks += xs 291 case x: 292 raise TypeError(type(x), i, block) 293 else: 294 # Remove the old blocks 295 library.remove(library.blocks[:]) 296 # Add the new blocks 297 library.add(blocks) 298 return library