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