hokora 0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hokora might be problematic. Click here for more details.
hokora/__init__.py
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import importlib.machinery
|
|
3
|
+
import importlib.util
|
|
4
|
+
import json
|
|
5
|
+
import pickle
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from io import StringIO
|
|
9
|
+
from os import chdir, environ, pathsep
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Callable, Generator, Optional, Type, get_type_hints
|
|
12
|
+
|
|
13
|
+
import jinja2
|
|
14
|
+
|
|
15
|
+
INCLUDE_PATH_ENVIRONMENT_VARIABLE = 'JINJA_TEMPLATE_DIRS'
|
|
16
|
+
PRELOAD_ENVIRONMENT_VARIABLE = 'HOKORA_MODULES'
|
|
17
|
+
|
|
18
|
+
PYTHON_MODULE_EXTENSIONS = (
|
|
19
|
+
'py',
|
|
20
|
+
'pyc',
|
|
21
|
+
'pyd',
|
|
22
|
+
'so',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
EPILOG = f"""
|
|
26
|
+
|
|
27
|
+
ENVIRONMENT VARIABLES
|
|
28
|
+
|
|
29
|
+
$JINJA_TEMPLATE_DIRS
|
|
30
|
+
A {pathsep}-separated list of additional template include paths, which
|
|
31
|
+
will be searched to handle {{%extends %}}, {{%include %}} and {{%import %}}
|
|
32
|
+
tags unless --no-include-env is passed at the command line.
|
|
33
|
+
|
|
34
|
+
$HOKORA_MODULES
|
|
35
|
+
Additional python modules (honoring $PYTHONPATH) to import at load time.
|
|
36
|
+
These can be used to extend the functionality of hokora if they contain
|
|
37
|
+
subclasses of DataFormat or HokoraFeature.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def dynamic_import(filename_or_module_name: str | Path, module_name=None):
|
|
42
|
+
if isinstance(filename_or_module_name, str):
|
|
43
|
+
try:
|
|
44
|
+
return __import__(filename_or_module_name)
|
|
45
|
+
except ModuleNotFoundError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
path = Path(filename_or_module_name).resolve()
|
|
50
|
+
except OSError:
|
|
51
|
+
raise ModuleNotFoundError(module_name or filename_or_module_name)
|
|
52
|
+
|
|
53
|
+
if not module_name:
|
|
54
|
+
module_name = path.name.split('.')[0]
|
|
55
|
+
|
|
56
|
+
if path.is_file() and not any(
|
|
57
|
+
path.name.endswith(f'.{x}') for x in PYTHON_MODULE_EXTENSIONS
|
|
58
|
+
):
|
|
59
|
+
# importlib's spec_from_file_location will balk unless it can
|
|
60
|
+
# recognize a file by extension, but shebang'd python scripts on unix
|
|
61
|
+
# systems won't necessarily have any extension.
|
|
62
|
+
loader0 = importlib.machinery.SourceFileLoader(module_name, str(path))
|
|
63
|
+
spec = importlib.util.spec_from_loader(module_name, loader0)
|
|
64
|
+
else:
|
|
65
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
66
|
+
if spec is None:
|
|
67
|
+
raise ModuleNotFoundError(module_name)
|
|
68
|
+
loader = spec.loader
|
|
69
|
+
if loader is None:
|
|
70
|
+
raise ImportError(module_name)
|
|
71
|
+
module = importlib.util.module_from_spec(spec)
|
|
72
|
+
sys.modules[module_name] = module
|
|
73
|
+
loader.exec_module(module)
|
|
74
|
+
return module
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HokoraFeature:
|
|
78
|
+
abstract: bool = True
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def find_all(cls) -> Generator[Type['HokoraFeature'], None, None]:
|
|
82
|
+
queue = [HokoraFeature.__subclasses__()]
|
|
83
|
+
while queue:
|
|
84
|
+
for cls in queue.pop(0):
|
|
85
|
+
if cls.__dict__.get('abstract'):
|
|
86
|
+
queue.append(cls.__subclasses__())
|
|
87
|
+
else:
|
|
88
|
+
cls.abstract = False
|
|
89
|
+
yield cls
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def configure_argparse(cls, parser: argparse.ArgumentParser):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def argparse_namespace_to_instance(
|
|
97
|
+
cls, args, template_path: Optional[Path] = None
|
|
98
|
+
) -> 'HokoraFeature':
|
|
99
|
+
hints = get_type_hints(cls)
|
|
100
|
+
return cls(
|
|
101
|
+
template_path=template_path,
|
|
102
|
+
**{k: v for k, v in args.__dict__.items() if k in hints},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def __init__(self, template_path: Optional[Path], **kw):
|
|
106
|
+
self.template_path = template_path
|
|
107
|
+
for k, v in kw.items():
|
|
108
|
+
setattr(self, k, v)
|
|
109
|
+
|
|
110
|
+
def update_jinja_include_paths(self, paths: list):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def update_jinja_environment_init_args(self, kw: dict):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def update_jinja_environment(self, env: jinja2.Environment):
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def update_template_context(self, context: dict):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def postprocess_output(self, output: str) -> str:
|
|
123
|
+
return output
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AbstractFeature(HokoraFeature):
|
|
127
|
+
abstract = True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class NonAbstractFeature(AbstractFeature):
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class DataFormat(HokoraFeature):
|
|
135
|
+
abstract: bool = True
|
|
136
|
+
extensions: tuple[str, ...] = tuple[str, ...]()
|
|
137
|
+
_dump: Optional[Callable] = None
|
|
138
|
+
name: str
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def filter_name(self) -> Optional[str]:
|
|
142
|
+
if self._dump or self.filter != DataFormat.filter:
|
|
143
|
+
return self.name
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def __init_subclass__(cls):
|
|
147
|
+
super().__init_subclass__()
|
|
148
|
+
if not cls.__dict__.get('name') and not cls.__dict__.get('abstract'):
|
|
149
|
+
cls.name = cls.__name__.replace('DataFormat', '').lower()
|
|
150
|
+
|
|
151
|
+
def load_as_context(self, path: Path, key: Optional[str]):
|
|
152
|
+
with open(path) as f:
|
|
153
|
+
loaded = self._load(f)
|
|
154
|
+
if isinstance(loaded, dict) or key is not None:
|
|
155
|
+
return loaded
|
|
156
|
+
if key is None:
|
|
157
|
+
key = path.name.split('.')[0]
|
|
158
|
+
return {key: loaded}
|
|
159
|
+
|
|
160
|
+
def _load(self, fd):
|
|
161
|
+
raise NotImplementedError(
|
|
162
|
+
f'Subclass {type(self)} must override _load or load_as_context'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def update_jinja_environment(self, env: jinja2.Environment):
|
|
166
|
+
if filter_name := self.filter_name():
|
|
167
|
+
env.filters[filter_name] = self.filter
|
|
168
|
+
|
|
169
|
+
def filter(self, value) -> str:
|
|
170
|
+
if self._dump is None:
|
|
171
|
+
raise NotImplementedError(
|
|
172
|
+
f'Subclass {type(self)} must override _dump or filter'
|
|
173
|
+
)
|
|
174
|
+
with StringIO() as sio:
|
|
175
|
+
self._dump(value, sio)
|
|
176
|
+
return sio.getvalue()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class JsonDataFormat(DataFormat):
|
|
180
|
+
extensions = ('json',)
|
|
181
|
+
_load = staticmethod(json.load)
|
|
182
|
+
_dump = staticmethod(json.dump)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class PythonDataFormat(DataFormat):
|
|
186
|
+
extensions = PYTHON_MODULE_EXTENSIONS
|
|
187
|
+
|
|
188
|
+
def load_as_context(self, path, key):
|
|
189
|
+
return dynamic_import(path, key).__dict__
|
|
190
|
+
|
|
191
|
+
def _dump(self, data, stream):
|
|
192
|
+
stream.write(repr(data))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class PickleDataFormat(DataFormat):
|
|
196
|
+
extensions = ('pkl', 'pickle')
|
|
197
|
+
_load = staticmethod(pickle.load)
|
|
198
|
+
_dump = staticmethod(pickle.dump)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
try:
|
|
203
|
+
import toml
|
|
204
|
+
|
|
205
|
+
_toml_dump = staticmethod(toml.dump)
|
|
206
|
+
except ModuleNotFoundError:
|
|
207
|
+
import tomllib as toml # type: ignore
|
|
208
|
+
except ModuleNotFoundError:
|
|
209
|
+
pass
|
|
210
|
+
else:
|
|
211
|
+
|
|
212
|
+
class TomlDataFormat(DataFormat):
|
|
213
|
+
extensions = ('toml',)
|
|
214
|
+
_load = staticmethod(toml.load)
|
|
215
|
+
try:
|
|
216
|
+
_dump = staticmethod(toml.dump)
|
|
217
|
+
except AttributeError:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
import yaml
|
|
223
|
+
except ModuleNotFoundError:
|
|
224
|
+
pass
|
|
225
|
+
else:
|
|
226
|
+
|
|
227
|
+
class YamlDataFormat(DataFormat):
|
|
228
|
+
extensions = ('yaml', 'yml')
|
|
229
|
+
_load = staticmethod(yaml.safe_load)
|
|
230
|
+
_dump = staticmethod(yaml.dump)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class ExternalContext(HokoraFeature):
|
|
234
|
+
context: dict
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def configure_argparse(cls, parser: argparse.ArgumentParser):
|
|
238
|
+
ctx = parser.add_argument_group('Jinja Context Variables')
|
|
239
|
+
ctx.add_argument(
|
|
240
|
+
'--context',
|
|
241
|
+
'-c',
|
|
242
|
+
action='append',
|
|
243
|
+
default=[],
|
|
244
|
+
help=(
|
|
245
|
+
'Read the given file for context values to supply as jinja '
|
|
246
|
+
'variables. Use key=filename if you want to namespace the '
|
|
247
|
+
'data, like {{key.somevar}}. See AVAILABLE CONTEXT FORMATS '
|
|
248
|
+
'below for supported file extensions.'
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
ctx.add_argument(
|
|
252
|
+
'--environ',
|
|
253
|
+
'-e',
|
|
254
|
+
dest='context',
|
|
255
|
+
action='append_const',
|
|
256
|
+
const=dict(environ),
|
|
257
|
+
help='Make environment variables available as jinja variables',
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def argparse_namespace_to_instance(
|
|
262
|
+
cls, args, template_path: Optional[Path] = None
|
|
263
|
+
) -> 'HokoraFeature':
|
|
264
|
+
context = {}
|
|
265
|
+
for entry in args.context:
|
|
266
|
+
if isinstance(entry, dict):
|
|
267
|
+
context.update(entry)
|
|
268
|
+
continue
|
|
269
|
+
if not isinstance(entry, str):
|
|
270
|
+
raise TypeError(f'{entry!r} is neither dict nor str')
|
|
271
|
+
key, equals, filename = entry.partition('=')
|
|
272
|
+
if not equals:
|
|
273
|
+
filename = key
|
|
274
|
+
key = ''
|
|
275
|
+
|
|
276
|
+
filename, *args = filename.split(pathsep)
|
|
277
|
+
kw = {}
|
|
278
|
+
for a in args:
|
|
279
|
+
k, eq, v = a.partition('=')
|
|
280
|
+
if eq:
|
|
281
|
+
kw[k] = v
|
|
282
|
+
elif k.startswith('-'):
|
|
283
|
+
kw[k[1:]] = False
|
|
284
|
+
else:
|
|
285
|
+
kw[k] = True
|
|
286
|
+
|
|
287
|
+
if not (ext := kw.pop('type', None)):
|
|
288
|
+
_, dot, ext = filename.rpartition('.')
|
|
289
|
+
if not dot:
|
|
290
|
+
raise ValueError(f"Can't detect format of {filename!r}")
|
|
291
|
+
|
|
292
|
+
for fmt in DataFormat.__subclasses__():
|
|
293
|
+
if ext.lower() in fmt.extensions:
|
|
294
|
+
loader_class = fmt
|
|
295
|
+
break
|
|
296
|
+
else:
|
|
297
|
+
raise ValueError(f'Unknown file type {ext}')
|
|
298
|
+
|
|
299
|
+
loader = loader_class(template_path=template_path, **kw)
|
|
300
|
+
value = loader.load_as_context(Path(filename), key)
|
|
301
|
+
|
|
302
|
+
if key:
|
|
303
|
+
context[key] = value
|
|
304
|
+
else:
|
|
305
|
+
context.update(value)
|
|
306
|
+
return cls(template_path=template_path, context=context)
|
|
307
|
+
|
|
308
|
+
def update_template_context(self, context: dict):
|
|
309
|
+
context.update(self.context)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class TemplateSearchPaths(HokoraFeature):
|
|
313
|
+
paths: list[Path]
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def configure_argparse(cls, parser: argparse.ArgumentParser):
|
|
317
|
+
group = parser.add_argument_group('Template search paths')
|
|
318
|
+
group.add_argument(
|
|
319
|
+
'-C',
|
|
320
|
+
help=(
|
|
321
|
+
'Change directories to this folder and interpret all paths as '
|
|
322
|
+
'relative to it.'
|
|
323
|
+
),
|
|
324
|
+
action='store',
|
|
325
|
+
)
|
|
326
|
+
group.add_argument(
|
|
327
|
+
'--no-include-env',
|
|
328
|
+
help=(
|
|
329
|
+
f"Don't parse ${INCLUDE_PATH_ENVIRONMENT_VARIABLE} for "
|
|
330
|
+
'include folders'
|
|
331
|
+
),
|
|
332
|
+
action='store_true',
|
|
333
|
+
)
|
|
334
|
+
group.add_argument(
|
|
335
|
+
'--no-include-cwd',
|
|
336
|
+
help=(
|
|
337
|
+
"Don't include the current working directory in the search "
|
|
338
|
+
'path'
|
|
339
|
+
),
|
|
340
|
+
action='store_true',
|
|
341
|
+
)
|
|
342
|
+
group.add_argument(
|
|
343
|
+
'--no-include-template-dir',
|
|
344
|
+
help="Don't include the template's folder in the search path.",
|
|
345
|
+
action='store_true',
|
|
346
|
+
)
|
|
347
|
+
group.add_argument(
|
|
348
|
+
'--include',
|
|
349
|
+
'-I',
|
|
350
|
+
help='Add this folder to the template include path',
|
|
351
|
+
action='append',
|
|
352
|
+
default=[],
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def argparse_namespace_to_instance(
|
|
357
|
+
cls, args, template_path: Optional[Path] = None
|
|
358
|
+
) -> 'HokoraFeature':
|
|
359
|
+
if args.C:
|
|
360
|
+
chdir(args.C)
|
|
361
|
+
paths = [Path(x).resolve() for x in args.include]
|
|
362
|
+
if not args.no_include_env:
|
|
363
|
+
env = environ.get(INCLUDE_PATH_ENVIRONMENT_VARIABLE, '')
|
|
364
|
+
paths.extend(Path(e) for e in env.split(pathsep) if e)
|
|
365
|
+
if template_path is not None and not args.no_include_template_dir:
|
|
366
|
+
paths.append(template_path.parent)
|
|
367
|
+
if not args.no_include_cwd:
|
|
368
|
+
paths.append(Path('.'))
|
|
369
|
+
|
|
370
|
+
return cls(
|
|
371
|
+
template_path=template_path,
|
|
372
|
+
paths=[p.resolve() for p in paths if p.is_dir()],
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def update_jinja_include_paths(self, paths: list):
|
|
376
|
+
paths.extend(self.paths)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class Autoescape(HokoraFeature):
|
|
380
|
+
autoescape: bool | Callable = jinja2.select_autoescape(
|
|
381
|
+
default_for_string=False
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
@classmethod
|
|
385
|
+
def configure_argparse(cls, parser: argparse.ArgumentParser):
|
|
386
|
+
parser.add_argument(
|
|
387
|
+
'--autoescape',
|
|
388
|
+
action=argparse.BooleanOptionalAction,
|
|
389
|
+
default=cls.autoescape,
|
|
390
|
+
help='whether to escape HTML entities in jinja expressions',
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def update_jinja_environment_init_args(self, kw):
|
|
394
|
+
kw['autoescape'] = self.autoescape
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class StripShebang(HokoraFeature):
|
|
398
|
+
strip_shebang: bool = False
|
|
399
|
+
|
|
400
|
+
@classmethod
|
|
401
|
+
def configure_argparse(cls, parser: argparse.ArgumentParser):
|
|
402
|
+
parser.add_argument(
|
|
403
|
+
'--strip-shebang',
|
|
404
|
+
action=argparse.BooleanOptionalAction,
|
|
405
|
+
help='remove the first line of the output if it is a bash shebang',
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def postprocess_output(self, output: str) -> str:
|
|
409
|
+
if self.strip_shebang and output.startswith('#!'):
|
|
410
|
+
return output.partition('\n')[-1]
|
|
411
|
+
return output
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def make_argument_parser():
|
|
415
|
+
formats = list[Type[DataFormat]]()
|
|
416
|
+
features = list[Type[HokoraFeature]]()
|
|
417
|
+
for feature_class in HokoraFeature.find_all():
|
|
418
|
+
if issubclass(feature_class, DataFormat):
|
|
419
|
+
formats.append(feature_class)
|
|
420
|
+
elif (
|
|
421
|
+
getattr(feature_class, '__doc__', '')
|
|
422
|
+
and feature_class.__module__ != __name__
|
|
423
|
+
):
|
|
424
|
+
features.append(feature_class)
|
|
425
|
+
|
|
426
|
+
with StringIO() as sio:
|
|
427
|
+
sio.write(EPILOG.replace('\n', '\n ', 1))
|
|
428
|
+
if features:
|
|
429
|
+
sio.write('\n \nENABLED FEATURES\n\n')
|
|
430
|
+
for f in features:
|
|
431
|
+
name = f.__name__
|
|
432
|
+
sio.write(f' {name}{getattr(f, "__doc__", "") or ""}\n')
|
|
433
|
+
sio.write('\n \nAVAILABLE CONTEXT FORMATS\n\n')
|
|
434
|
+
for f in formats:
|
|
435
|
+
exts = ', '.join(f'*.{x}' for x in f.extensions)
|
|
436
|
+
with StringIO('%74s' % (exts,)) as name_builder:
|
|
437
|
+
if filter_name := f.filter_name():
|
|
438
|
+
name_builder.seek(20)
|
|
439
|
+
name_builder.write('{{ ... | %s }}' % (filter_name,))
|
|
440
|
+
name_builder.seek(0)
|
|
441
|
+
name_builder.write(f.name)
|
|
442
|
+
name = name_builder.getvalue()
|
|
443
|
+
sio.write(f' {name}{getattr(f, "__doc__", "") or ""}\n')
|
|
444
|
+
main = argparse.ArgumentParser(
|
|
445
|
+
description='Hokora: Standalone Jinja2 template renderer',
|
|
446
|
+
epilog=sio.getvalue(),
|
|
447
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
448
|
+
)
|
|
449
|
+
main.add_argument('template', nargs='?', help='path to template to render')
|
|
450
|
+
for feature_class in HokoraFeature.find_all():
|
|
451
|
+
feature_class.configure_argparse(main)
|
|
452
|
+
return main
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class ExplicitPathTemplate(str):
|
|
456
|
+
path: Path
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class PathLoader(jinja2.FileSystemLoader):
|
|
460
|
+
def get_source(self, environment, template):
|
|
461
|
+
if isinstance(template, ExplicitPathTemplate):
|
|
462
|
+
with open(template.path) as fd:
|
|
463
|
+
return fd.read(), template, True
|
|
464
|
+
return super().get_source(environment, template)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class HelpfulDefault(HokoraFeature):
|
|
468
|
+
_filters: dict[str, Callable] = {'repr': repr}
|
|
469
|
+
_context: dict[str, Any] = {'hokora': sys.modules[__name__]}
|
|
470
|
+
|
|
471
|
+
@classmethod
|
|
472
|
+
def filter(cls, func):
|
|
473
|
+
cls._filters[func.__name__] = func
|
|
474
|
+
return func
|
|
475
|
+
|
|
476
|
+
@classmethod
|
|
477
|
+
def function(cls, func):
|
|
478
|
+
cls._context[func.__name__] = func
|
|
479
|
+
return func
|
|
480
|
+
|
|
481
|
+
def update_jinja_environment(self, env: jinja2.Environment):
|
|
482
|
+
for k, v in self._filters.items():
|
|
483
|
+
env.filters.setdefault(k, v)
|
|
484
|
+
|
|
485
|
+
def update_template_context(self, context: dict):
|
|
486
|
+
context.setdefault('template_path', self.template_path)
|
|
487
|
+
for k, v in self._context.items():
|
|
488
|
+
context.setdefault(k, v)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@HelpfulDefault.filter
|
|
492
|
+
def sub(text, pat, replacement, *a, **kw):
|
|
493
|
+
return re.sub(pat, replacement, str(text), *a, **kw)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@HelpfulDefault.filter
|
|
497
|
+
def snip(text, start, end=None, index=0, inclusive=None):
|
|
498
|
+
if inclusive is None:
|
|
499
|
+
if isinstance(index, (bool, tuple)):
|
|
500
|
+
inclusive = index
|
|
501
|
+
index = 0
|
|
502
|
+
else:
|
|
503
|
+
inclusive = (isinstance(start, int), False)
|
|
504
|
+
if isinstance(inclusive, bool):
|
|
505
|
+
inclusive = (inclusive, inclusive)
|
|
506
|
+
if isinstance(start, int) or isinstance(end, int) or end is None:
|
|
507
|
+
if index:
|
|
508
|
+
raise ValueError(
|
|
509
|
+
"|snip's index= argument only works with text markers"
|
|
510
|
+
)
|
|
511
|
+
buf = list[str]()
|
|
512
|
+
on = False
|
|
513
|
+
for n, line in enumerate(str(text).split('\n'), start=1):
|
|
514
|
+
if on:
|
|
515
|
+
if (isinstance(end, int) and n >= end) or (
|
|
516
|
+
isinstance(end, str) and re.search(end, line)
|
|
517
|
+
):
|
|
518
|
+
on = False
|
|
519
|
+
if index:
|
|
520
|
+
index -= 1
|
|
521
|
+
buf.clear()
|
|
522
|
+
else:
|
|
523
|
+
buf.append(line if inclusive[1] else '')
|
|
524
|
+
return '\n'.join(buf)
|
|
525
|
+
else:
|
|
526
|
+
if (isinstance(start, int) and n == start) or (
|
|
527
|
+
isinstance(start, str) and re.search(start, line)
|
|
528
|
+
):
|
|
529
|
+
on = True
|
|
530
|
+
if not inclusive[0]:
|
|
531
|
+
continue
|
|
532
|
+
if on:
|
|
533
|
+
buf.append(line)
|
|
534
|
+
if on and end is None:
|
|
535
|
+
return '\n'.join(buf)
|
|
536
|
+
return ''
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@HelpfulDefault.function
|
|
540
|
+
def read(path):
|
|
541
|
+
with open(path) as fd:
|
|
542
|
+
return fd.read()
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def create_jinja_environment(features: list[HokoraFeature]):
|
|
546
|
+
include_paths = list[Path]()
|
|
547
|
+
for feat in features:
|
|
548
|
+
feat.update_jinja_include_paths(include_paths)
|
|
549
|
+
|
|
550
|
+
environment_kw = {
|
|
551
|
+
'loader': PathLoader(include_paths),
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
for feat in features:
|
|
555
|
+
feat.update_jinja_environment_init_args(environment_kw)
|
|
556
|
+
|
|
557
|
+
environment = jinja2.Environment(**environment_kw) # type: ignore
|
|
558
|
+
|
|
559
|
+
for feat in features:
|
|
560
|
+
feat.update_jinja_environment(environment)
|
|
561
|
+
|
|
562
|
+
return environment
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def args_to_templates(
|
|
566
|
+
args,
|
|
567
|
+
) -> list[tuple[jinja2.Template, list[HokoraFeature], Optional[Path]]]:
|
|
568
|
+
if args.template and args.template != '-':
|
|
569
|
+
template_path = Path(args.template).resolve()
|
|
570
|
+
if not template_path.exists():
|
|
571
|
+
raise FileNotFoundError(args.template)
|
|
572
|
+
else:
|
|
573
|
+
template_path = None
|
|
574
|
+
|
|
575
|
+
features = [
|
|
576
|
+
feat.argparse_namespace_to_instance(args, template_path)
|
|
577
|
+
for feat in HokoraFeature.find_all()
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
environment = create_jinja_environment(features)
|
|
581
|
+
template: jinja2.Template
|
|
582
|
+
if template_path is None:
|
|
583
|
+
if sys.stdin.isatty():
|
|
584
|
+
eof = 'Ctrl+Z, Enter' if sys.platform == 'win32' else 'Ctrl+D'
|
|
585
|
+
sys.stderr.write(f'Type your template ({eof} when done)\n')
|
|
586
|
+
sys.stderr.flush()
|
|
587
|
+
template = environment.from_string(sys.stdin.read())
|
|
588
|
+
else:
|
|
589
|
+
ept = ExplicitPathTemplate(template_path)
|
|
590
|
+
ept.path = template_path
|
|
591
|
+
template = environment.get_template(ept)
|
|
592
|
+
|
|
593
|
+
return [(template, features, None)]
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def main():
|
|
597
|
+
for mod in environ.get(PRELOAD_ENVIRONMENT_VARIABLE, '').split(pathsep):
|
|
598
|
+
if mod:
|
|
599
|
+
dynamic_import(mod)
|
|
600
|
+
args = make_argument_parser().parse_args()
|
|
601
|
+
for template, features, render_dest in args_to_templates(args):
|
|
602
|
+
assert not render_dest, 'TODO: support rendering to file'
|
|
603
|
+
context: dict[str, Any] = {}
|
|
604
|
+
for feat in features:
|
|
605
|
+
feat.update_template_context(context)
|
|
606
|
+
|
|
607
|
+
rendered = template.render(context)
|
|
608
|
+
|
|
609
|
+
for feat in features:
|
|
610
|
+
rendered = feat.postprocess_output(rendered)
|
|
611
|
+
|
|
612
|
+
print(rendered, end='')
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
if __name__ == '__main__':
|
|
616
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hokora
|
|
3
|
+
Version: 0.dev0
|
|
4
|
+
License-Expression: BSD-3-Clause
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/drmoose/hokora
|
|
6
|
+
Project-URL: Issues, https://gitlab.com/drmoose/hokora/-/issues
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: COPYING
|
|
10
|
+
Requires-Dist: jinja2
|
|
11
|
+
Provides-Extra: check
|
|
12
|
+
Requires-Dist: ruff; extra == "check"
|
|
13
|
+
Requires-Dist: mypy; extra == "check"
|
|
14
|
+
Provides-Extra: toml
|
|
15
|
+
Requires-Dist: toml; extra == "toml"
|
|
16
|
+
Provides-Extra: yaml
|
|
17
|
+
Requires-Dist: PyYAML; extra == "yaml"
|
|
18
|
+
Provides-Extra: all
|
|
19
|
+
Requires-Dist: hokora[toml,yaml]; extra == "all"
|
|
20
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
hokora/__init__.py,sha256=hx1YSvFpnDOHPDjAXmZg_pR2ZGfP7aFOt6JTtudpI_k,18916
|
|
2
|
+
hokora-0.dev0.dist-info/licenses/COPYING,sha256=XYeZfYzmU_j8eZTy3Jl7YOOAX1wtNzLbO5v2I3QqQJs,1483
|
|
3
|
+
hokora-0.dev0.dist-info/METADATA,sha256=zNFbjL64q4JXpp1wzASM-vRbJGGtj9xy4Bk01S8F_68,620
|
|
4
|
+
hokora-0.dev0.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
5
|
+
hokora-0.dev0.dist-info/entry_points.txt,sha256=AXnUrRzHdBXsdKD8pQ6yYhuaBo0xKUngFyeZj2w0eYI,39
|
|
6
|
+
hokora-0.dev0.dist-info/top_level.txt,sha256=qOxZ02jJIbIVPVAAhK0vALePCOuLfElvYagg5jvKQ9w,7
|
|
7
|
+
hokora-0.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Copyright (c) 2025 drmoose.net
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are
|
|
5
|
+
met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright
|
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright
|
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
|
12
|
+
documentation and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
19
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
20
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
21
|
+
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
22
|
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
23
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
24
|
+
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
25
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
26
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
27
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
28
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hokora
|