okstra 0.31.0 → 0.32.0

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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/runtime/BUILD.json +2 -2
  3. package/runtime/agents/SKILL.md +3 -3
  4. package/runtime/agents/workers/report-writer-worker.md +45 -67
  5. package/runtime/bin/okstra-render-final-report.py +101 -0
  6. package/runtime/bin/okstra-render-report-views.py +17 -10
  7. package/runtime/bin/okstra-token-usage.py +3 -1
  8. package/runtime/python/okstra_ctl/final_report_schema.py +253 -0
  9. package/runtime/python/okstra_ctl/render_final_report.py +201 -0
  10. package/runtime/python/okstra_ctl/report_views.py +108 -305
  11. package/runtime/python/okstra_token_usage/__init__.py +5 -1
  12. package/runtime/python/okstra_token_usage/cli.py +66 -36
  13. package/runtime/python/okstra_token_usage/report.py +148 -65
  14. package/runtime/python/okstra_vendor/__init__.py +37 -0
  15. package/runtime/python/okstra_vendor/jinja2/__init__.py +38 -0
  16. package/runtime/python/okstra_vendor/jinja2/_identifier.py +6 -0
  17. package/runtime/python/okstra_vendor/jinja2/async_utils.py +99 -0
  18. package/runtime/python/okstra_vendor/jinja2/bccache.py +408 -0
  19. package/runtime/python/okstra_vendor/jinja2/compiler.py +1998 -0
  20. package/runtime/python/okstra_vendor/jinja2/constants.py +20 -0
  21. package/runtime/python/okstra_vendor/jinja2/debug.py +191 -0
  22. package/runtime/python/okstra_vendor/jinja2/defaults.py +48 -0
  23. package/runtime/python/okstra_vendor/jinja2/environment.py +1672 -0
  24. package/runtime/python/okstra_vendor/jinja2/exceptions.py +166 -0
  25. package/runtime/python/okstra_vendor/jinja2/ext.py +870 -0
  26. package/runtime/python/okstra_vendor/jinja2/filters.py +1873 -0
  27. package/runtime/python/okstra_vendor/jinja2/idtracking.py +318 -0
  28. package/runtime/python/okstra_vendor/jinja2/lexer.py +868 -0
  29. package/runtime/python/okstra_vendor/jinja2/loaders.py +693 -0
  30. package/runtime/python/okstra_vendor/jinja2/meta.py +112 -0
  31. package/runtime/python/okstra_vendor/jinja2/nativetypes.py +130 -0
  32. package/runtime/python/okstra_vendor/jinja2/nodes.py +1206 -0
  33. package/runtime/python/okstra_vendor/jinja2/optimizer.py +48 -0
  34. package/runtime/python/okstra_vendor/jinja2/parser.py +1049 -0
  35. package/runtime/python/okstra_vendor/jinja2/py.typed +0 -0
  36. package/runtime/python/okstra_vendor/jinja2/runtime.py +1062 -0
  37. package/runtime/python/okstra_vendor/jinja2/sandbox.py +436 -0
  38. package/runtime/python/okstra_vendor/jinja2/tests.py +256 -0
  39. package/runtime/python/okstra_vendor/jinja2/utils.py +766 -0
  40. package/runtime/python/okstra_vendor/jinja2/visitor.py +92 -0
  41. package/runtime/python/okstra_vendor/markupsafe/__init__.py +396 -0
  42. package/runtime/python/okstra_vendor/markupsafe/_native.py +8 -0
  43. package/runtime/python/okstra_vendor/markupsafe/py.typed +0 -0
  44. package/runtime/schemas/final-report-v1.0.schema.json +1391 -0
  45. package/runtime/skills/okstra-report-writer/SKILL.md +29 -28
  46. package/runtime/templates/reports/final-report.template.md +370 -411
  47. package/runtime/templates/reports/report.css +12 -6
  48. package/runtime/validators/lib/fixtures.sh +7 -7
  49. package/runtime/validators/validate-report-views.py +24 -153
  50. package/runtime/validators/validate-run.py +102 -19
  51. package/src/install.mjs +20 -1
@@ -0,0 +1,1062 @@
1
+ """The runtime functions and state used by compiled templates."""
2
+
3
+ import functools
4
+ import sys
5
+ import typing as t
6
+ from collections import abc
7
+ from itertools import chain
8
+
9
+ from markupsafe import escape # noqa: F401
10
+ from markupsafe import Markup
11
+ from markupsafe import soft_str
12
+
13
+ from .async_utils import auto_aiter
14
+ from .async_utils import auto_await # noqa: F401
15
+ from .exceptions import TemplateNotFound # noqa: F401
16
+ from .exceptions import TemplateRuntimeError # noqa: F401
17
+ from .exceptions import UndefinedError
18
+ from .nodes import EvalContext
19
+ from .utils import _PassArg
20
+ from .utils import concat
21
+ from .utils import internalcode
22
+ from .utils import missing
23
+ from .utils import Namespace # noqa: F401
24
+ from .utils import object_type_repr
25
+ from .utils import pass_eval_context
26
+
27
+ V = t.TypeVar("V")
28
+ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
29
+
30
+ if t.TYPE_CHECKING:
31
+ import logging
32
+
33
+ import typing_extensions as te
34
+
35
+ from .environment import Environment
36
+
37
+ class LoopRenderFunc(te.Protocol):
38
+ def __call__(
39
+ self,
40
+ reciter: t.Iterable[V],
41
+ loop_render_func: "LoopRenderFunc",
42
+ depth: int = 0,
43
+ ) -> str: ...
44
+
45
+
46
+ # these variables are exported to the template runtime
47
+ exported = [
48
+ "LoopContext",
49
+ "TemplateReference",
50
+ "Macro",
51
+ "Markup",
52
+ "TemplateRuntimeError",
53
+ "missing",
54
+ "escape",
55
+ "markup_join",
56
+ "str_join",
57
+ "identity",
58
+ "TemplateNotFound",
59
+ "Namespace",
60
+ "Undefined",
61
+ "internalcode",
62
+ ]
63
+ async_exported = [
64
+ "AsyncLoopContext",
65
+ "auto_aiter",
66
+ "auto_await",
67
+ ]
68
+
69
+
70
+ def identity(x: V) -> V:
71
+ """Returns its argument. Useful for certain things in the
72
+ environment.
73
+ """
74
+ return x
75
+
76
+
77
+ def markup_join(seq: t.Iterable[t.Any]) -> str:
78
+ """Concatenation that escapes if necessary and converts to string."""
79
+ buf = []
80
+ iterator = map(soft_str, seq)
81
+ for arg in iterator:
82
+ buf.append(arg)
83
+ if hasattr(arg, "__html__"):
84
+ return Markup("").join(chain(buf, iterator))
85
+ return concat(buf)
86
+
87
+
88
+ def str_join(seq: t.Iterable[t.Any]) -> str:
89
+ """Simple args to string conversion and concatenation."""
90
+ return concat(map(str, seq))
91
+
92
+
93
+ def new_context(
94
+ environment: "Environment",
95
+ template_name: t.Optional[str],
96
+ blocks: t.Dict[str, t.Callable[["Context"], t.Iterator[str]]],
97
+ vars: t.Optional[t.Dict[str, t.Any]] = None,
98
+ shared: bool = False,
99
+ globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
100
+ locals: t.Optional[t.Mapping[str, t.Any]] = None,
101
+ ) -> "Context":
102
+ """Internal helper for context creation."""
103
+ if vars is None:
104
+ vars = {}
105
+ if shared:
106
+ parent = vars
107
+ else:
108
+ parent = dict(globals or (), **vars)
109
+ if locals:
110
+ # if the parent is shared a copy should be created because
111
+ # we don't want to modify the dict passed
112
+ if shared:
113
+ parent = dict(parent)
114
+ for key, value in locals.items():
115
+ if value is not missing:
116
+ parent[key] = value
117
+ return environment.context_class(
118
+ environment, parent, template_name, blocks, globals=globals
119
+ )
120
+
121
+
122
+ class TemplateReference:
123
+ """The `self` in templates."""
124
+
125
+ def __init__(self, context: "Context") -> None:
126
+ self.__context = context
127
+
128
+ def __getitem__(self, name: str) -> t.Any:
129
+ blocks = self.__context.blocks[name]
130
+ return BlockReference(name, self.__context, blocks, 0)
131
+
132
+ def __repr__(self) -> str:
133
+ return f"<{type(self).__name__} {self.__context.name!r}>"
134
+
135
+
136
+ def _dict_method_all(dict_method: F) -> F:
137
+ @functools.wraps(dict_method)
138
+ def f_all(self: "Context") -> t.Any:
139
+ return dict_method(self.get_all())
140
+
141
+ return t.cast(F, f_all)
142
+
143
+
144
+ @abc.Mapping.register
145
+ class Context:
146
+ """The template context holds the variables of a template. It stores the
147
+ values passed to the template and also the names the template exports.
148
+ Creating instances is neither supported nor useful as it's created
149
+ automatically at various stages of the template evaluation and should not
150
+ be created by hand.
151
+
152
+ The context is immutable. Modifications on :attr:`parent` **must not**
153
+ happen and modifications on :attr:`vars` are allowed from generated
154
+ template code only. Template filters and global functions marked as
155
+ :func:`pass_context` get the active context passed as first argument
156
+ and are allowed to access the context read-only.
157
+
158
+ The template context supports read only dict operations (`get`,
159
+ `keys`, `values`, `items`, `iterkeys`, `itervalues`, `iteritems`,
160
+ `__getitem__`, `__contains__`). Additionally there is a :meth:`resolve`
161
+ method that doesn't fail with a `KeyError` but returns an
162
+ :class:`Undefined` object for missing variables.
163
+ """
164
+
165
+ def __init__(
166
+ self,
167
+ environment: "Environment",
168
+ parent: t.Dict[str, t.Any],
169
+ name: t.Optional[str],
170
+ blocks: t.Dict[str, t.Callable[["Context"], t.Iterator[str]]],
171
+ globals: t.Optional[t.MutableMapping[str, t.Any]] = None,
172
+ ):
173
+ self.parent = parent
174
+ self.vars: t.Dict[str, t.Any] = {}
175
+ self.environment: Environment = environment
176
+ self.eval_ctx = EvalContext(self.environment, name)
177
+ self.exported_vars: t.Set[str] = set()
178
+ self.name = name
179
+ self.globals_keys = set() if globals is None else set(globals)
180
+
181
+ # create the initial mapping of blocks. Whenever template inheritance
182
+ # takes place the runtime will update this mapping with the new blocks
183
+ # from the template.
184
+ self.blocks = {k: [v] for k, v in blocks.items()}
185
+
186
+ def super(
187
+ self, name: str, current: t.Callable[["Context"], t.Iterator[str]]
188
+ ) -> t.Union["BlockReference", "Undefined"]:
189
+ """Render a parent block."""
190
+ try:
191
+ blocks = self.blocks[name]
192
+ index = blocks.index(current) + 1
193
+ blocks[index]
194
+ except LookupError:
195
+ return self.environment.undefined(
196
+ f"there is no parent block called {name!r}.", name="super"
197
+ )
198
+ return BlockReference(name, self, blocks, index)
199
+
200
+ def get(self, key: str, default: t.Any = None) -> t.Any:
201
+ """Look up a variable by name, or return a default if the key is
202
+ not found.
203
+
204
+ :param key: The variable name to look up.
205
+ :param default: The value to return if the key is not found.
206
+ """
207
+ try:
208
+ return self[key]
209
+ except KeyError:
210
+ return default
211
+
212
+ def resolve(self, key: str) -> t.Union[t.Any, "Undefined"]:
213
+ """Look up a variable by name, or return an :class:`Undefined`
214
+ object if the key is not found.
215
+
216
+ If you need to add custom behavior, override
217
+ :meth:`resolve_or_missing`, not this method. The various lookup
218
+ functions use that method, not this one.
219
+
220
+ :param key: The variable name to look up.
221
+ """
222
+ rv = self.resolve_or_missing(key)
223
+
224
+ if rv is missing:
225
+ return self.environment.undefined(name=key)
226
+
227
+ return rv
228
+
229
+ def resolve_or_missing(self, key: str) -> t.Any:
230
+ """Look up a variable by name, or return a ``missing`` sentinel
231
+ if the key is not found.
232
+
233
+ Override this method to add custom lookup behavior.
234
+ :meth:`resolve`, :meth:`get`, and :meth:`__getitem__` use this
235
+ method. Don't call this method directly.
236
+
237
+ :param key: The variable name to look up.
238
+ """
239
+ if key in self.vars:
240
+ return self.vars[key]
241
+
242
+ if key in self.parent:
243
+ return self.parent[key]
244
+
245
+ return missing
246
+
247
+ def get_exported(self) -> t.Dict[str, t.Any]:
248
+ """Get a new dict with the exported variables."""
249
+ return {k: self.vars[k] for k in self.exported_vars}
250
+
251
+ def get_all(self) -> t.Dict[str, t.Any]:
252
+ """Return the complete context as dict including the exported
253
+ variables. For optimizations reasons this might not return an
254
+ actual copy so be careful with using it.
255
+ """
256
+ if not self.vars:
257
+ return self.parent
258
+ if not self.parent:
259
+ return self.vars
260
+ return dict(self.parent, **self.vars)
261
+
262
+ @internalcode
263
+ def call(
264
+ __self,
265
+ __obj: t.Callable[..., t.Any],
266
+ *args: t.Any,
267
+ **kwargs: t.Any, # noqa: B902
268
+ ) -> t.Union[t.Any, "Undefined"]:
269
+ """Call the callable with the arguments and keyword arguments
270
+ provided but inject the active context or environment as first
271
+ argument if the callable has :func:`pass_context` or
272
+ :func:`pass_environment`.
273
+ """
274
+ if __debug__:
275
+ __traceback_hide__ = True # noqa
276
+
277
+ # Allow callable classes to take a context
278
+ if (
279
+ hasattr(__obj, "__call__") # noqa: B004
280
+ and _PassArg.from_obj(__obj.__call__) is not None
281
+ ):
282
+ __obj = __obj.__call__
283
+
284
+ pass_arg = _PassArg.from_obj(__obj)
285
+
286
+ if pass_arg is _PassArg.context:
287
+ # the active context should have access to variables set in
288
+ # loops and blocks without mutating the context itself
289
+ if kwargs.get("_loop_vars"):
290
+ __self = __self.derived(kwargs["_loop_vars"])
291
+ if kwargs.get("_block_vars"):
292
+ __self = __self.derived(kwargs["_block_vars"])
293
+ args = (__self,) + args
294
+ elif pass_arg is _PassArg.eval_context:
295
+ args = (__self.eval_ctx,) + args
296
+ elif pass_arg is _PassArg.environment:
297
+ args = (__self.environment,) + args
298
+
299
+ kwargs.pop("_block_vars", None)
300
+ kwargs.pop("_loop_vars", None)
301
+
302
+ try:
303
+ return __obj(*args, **kwargs)
304
+ except StopIteration:
305
+ return __self.environment.undefined(
306
+ "value was undefined because a callable raised a"
307
+ " StopIteration exception"
308
+ )
309
+
310
+ def derived(self, locals: t.Optional[t.Dict[str, t.Any]] = None) -> "Context":
311
+ """Internal helper function to create a derived context. This is
312
+ used in situations where the system needs a new context in the same
313
+ template that is independent.
314
+ """
315
+ context = new_context(
316
+ self.environment, self.name, {}, self.get_all(), True, None, locals
317
+ )
318
+ context.eval_ctx = self.eval_ctx
319
+ context.blocks.update((k, list(v)) for k, v in self.blocks.items())
320
+ return context
321
+
322
+ keys = _dict_method_all(dict.keys)
323
+ values = _dict_method_all(dict.values)
324
+ items = _dict_method_all(dict.items)
325
+
326
+ def __contains__(self, name: str) -> bool:
327
+ return name in self.vars or name in self.parent
328
+
329
+ def __getitem__(self, key: str) -> t.Any:
330
+ """Look up a variable by name with ``[]`` syntax, or raise a
331
+ ``KeyError`` if the key is not found.
332
+ """
333
+ item = self.resolve_or_missing(key)
334
+
335
+ if item is missing:
336
+ raise KeyError(key)
337
+
338
+ return item
339
+
340
+ def __repr__(self) -> str:
341
+ return f"<{type(self).__name__} {self.get_all()!r} of {self.name!r}>"
342
+
343
+
344
+ class BlockReference:
345
+ """One block on a template reference."""
346
+
347
+ def __init__(
348
+ self,
349
+ name: str,
350
+ context: "Context",
351
+ stack: t.List[t.Callable[["Context"], t.Iterator[str]]],
352
+ depth: int,
353
+ ) -> None:
354
+ self.name = name
355
+ self._context = context
356
+ self._stack = stack
357
+ self._depth = depth
358
+
359
+ @property
360
+ def super(self) -> t.Union["BlockReference", "Undefined"]:
361
+ """Super the block."""
362
+ if self._depth + 1 >= len(self._stack):
363
+ return self._context.environment.undefined(
364
+ f"there is no parent block called {self.name!r}.", name="super"
365
+ )
366
+ return BlockReference(self.name, self._context, self._stack, self._depth + 1)
367
+
368
+ @internalcode
369
+ async def _async_call(self) -> str:
370
+ rv = self._context.environment.concat( # type: ignore
371
+ [x async for x in self._stack[self._depth](self._context)] # type: ignore
372
+ )
373
+
374
+ if self._context.eval_ctx.autoescape:
375
+ return Markup(rv)
376
+
377
+ return rv
378
+
379
+ @internalcode
380
+ def __call__(self) -> str:
381
+ if self._context.environment.is_async:
382
+ return self._async_call() # type: ignore
383
+
384
+ rv = self._context.environment.concat( # type: ignore
385
+ self._stack[self._depth](self._context)
386
+ )
387
+
388
+ if self._context.eval_ctx.autoescape:
389
+ return Markup(rv)
390
+
391
+ return rv
392
+
393
+
394
+ class LoopContext:
395
+ """A wrapper iterable for dynamic ``for`` loops, with information
396
+ about the loop and iteration.
397
+ """
398
+
399
+ #: Current iteration of the loop, starting at 0.
400
+ index0 = -1
401
+
402
+ _length: t.Optional[int] = None
403
+ _after: t.Any = missing
404
+ _current: t.Any = missing
405
+ _before: t.Any = missing
406
+ _last_changed_value: t.Any = missing
407
+
408
+ def __init__(
409
+ self,
410
+ iterable: t.Iterable[V],
411
+ undefined: t.Type["Undefined"],
412
+ recurse: t.Optional["LoopRenderFunc"] = None,
413
+ depth0: int = 0,
414
+ ) -> None:
415
+ """
416
+ :param iterable: Iterable to wrap.
417
+ :param undefined: :class:`Undefined` class to use for next and
418
+ previous items.
419
+ :param recurse: The function to render the loop body when the
420
+ loop is marked recursive.
421
+ :param depth0: Incremented when looping recursively.
422
+ """
423
+ self._iterable = iterable
424
+ self._iterator = self._to_iterator(iterable)
425
+ self._undefined = undefined
426
+ self._recurse = recurse
427
+ #: How many levels deep a recursive loop currently is, starting at 0.
428
+ self.depth0 = depth0
429
+
430
+ @staticmethod
431
+ def _to_iterator(iterable: t.Iterable[V]) -> t.Iterator[V]:
432
+ return iter(iterable)
433
+
434
+ @property
435
+ def length(self) -> int:
436
+ """Length of the iterable.
437
+
438
+ If the iterable is a generator or otherwise does not have a
439
+ size, it is eagerly evaluated to get a size.
440
+ """
441
+ if self._length is not None:
442
+ return self._length
443
+
444
+ try:
445
+ self._length = len(self._iterable) # type: ignore
446
+ except TypeError:
447
+ iterable = list(self._iterator)
448
+ self._iterator = self._to_iterator(iterable)
449
+ self._length = len(iterable) + self.index + (self._after is not missing)
450
+
451
+ return self._length
452
+
453
+ def __len__(self) -> int:
454
+ return self.length
455
+
456
+ @property
457
+ def depth(self) -> int:
458
+ """How many levels deep a recursive loop currently is, starting at 1."""
459
+ return self.depth0 + 1
460
+
461
+ @property
462
+ def index(self) -> int:
463
+ """Current iteration of the loop, starting at 1."""
464
+ return self.index0 + 1
465
+
466
+ @property
467
+ def revindex0(self) -> int:
468
+ """Number of iterations from the end of the loop, ending at 0.
469
+
470
+ Requires calculating :attr:`length`.
471
+ """
472
+ return self.length - self.index
473
+
474
+ @property
475
+ def revindex(self) -> int:
476
+ """Number of iterations from the end of the loop, ending at 1.
477
+
478
+ Requires calculating :attr:`length`.
479
+ """
480
+ return self.length - self.index0
481
+
482
+ @property
483
+ def first(self) -> bool:
484
+ """Whether this is the first iteration of the loop."""
485
+ return self.index0 == 0
486
+
487
+ def _peek_next(self) -> t.Any:
488
+ """Return the next element in the iterable, or :data:`missing`
489
+ if the iterable is exhausted. Only peeks one item ahead, caching
490
+ the result in :attr:`_last` for use in subsequent checks. The
491
+ cache is reset when :meth:`__next__` is called.
492
+ """
493
+ if self._after is not missing:
494
+ return self._after
495
+
496
+ self._after = next(self._iterator, missing)
497
+ return self._after
498
+
499
+ @property
500
+ def last(self) -> bool:
501
+ """Whether this is the last iteration of the loop.
502
+
503
+ Causes the iterable to advance early. See
504
+ :func:`itertools.groupby` for issues this can cause.
505
+ The :func:`groupby` filter avoids that issue.
506
+ """
507
+ return self._peek_next() is missing
508
+
509
+ @property
510
+ def previtem(self) -> t.Union[t.Any, "Undefined"]:
511
+ """The item in the previous iteration. Undefined during the
512
+ first iteration.
513
+ """
514
+ if self.first:
515
+ return self._undefined("there is no previous item")
516
+
517
+ return self._before
518
+
519
+ @property
520
+ def nextitem(self) -> t.Union[t.Any, "Undefined"]:
521
+ """The item in the next iteration. Undefined during the last
522
+ iteration.
523
+
524
+ Causes the iterable to advance early. See
525
+ :func:`itertools.groupby` for issues this can cause.
526
+ The :func:`jinja-filters.groupby` filter avoids that issue.
527
+ """
528
+ rv = self._peek_next()
529
+
530
+ if rv is missing:
531
+ return self._undefined("there is no next item")
532
+
533
+ return rv
534
+
535
+ def cycle(self, *args: V) -> V:
536
+ """Return a value from the given args, cycling through based on
537
+ the current :attr:`index0`.
538
+
539
+ :param args: One or more values to cycle through.
540
+ """
541
+ if not args:
542
+ raise TypeError("no items for cycling given")
543
+
544
+ return args[self.index0 % len(args)]
545
+
546
+ def changed(self, *value: t.Any) -> bool:
547
+ """Return ``True`` if previously called with a different value
548
+ (including when called for the first time).
549
+
550
+ :param value: One or more values to compare to the last call.
551
+ """
552
+ if self._last_changed_value != value:
553
+ self._last_changed_value = value
554
+ return True
555
+
556
+ return False
557
+
558
+ def __iter__(self) -> "LoopContext":
559
+ return self
560
+
561
+ def __next__(self) -> t.Tuple[t.Any, "LoopContext"]:
562
+ if self._after is not missing:
563
+ rv = self._after
564
+ self._after = missing
565
+ else:
566
+ rv = next(self._iterator)
567
+
568
+ self.index0 += 1
569
+ self._before = self._current
570
+ self._current = rv
571
+ return rv, self
572
+
573
+ @internalcode
574
+ def __call__(self, iterable: t.Iterable[V]) -> str:
575
+ """When iterating over nested data, render the body of the loop
576
+ recursively with the given inner iterable data.
577
+
578
+ The loop must have the ``recursive`` marker for this to work.
579
+ """
580
+ if self._recurse is None:
581
+ raise TypeError(
582
+ "The loop must have the 'recursive' marker to be called recursively."
583
+ )
584
+
585
+ return self._recurse(iterable, self._recurse, depth=self.depth)
586
+
587
+ def __repr__(self) -> str:
588
+ return f"<{type(self).__name__} {self.index}/{self.length}>"
589
+
590
+
591
+ class AsyncLoopContext(LoopContext):
592
+ _iterator: t.AsyncIterator[t.Any] # type: ignore
593
+
594
+ @staticmethod
595
+ def _to_iterator( # type: ignore
596
+ iterable: t.Union[t.Iterable[V], t.AsyncIterable[V]],
597
+ ) -> t.AsyncIterator[V]:
598
+ return auto_aiter(iterable)
599
+
600
+ @property
601
+ async def length(self) -> int: # type: ignore
602
+ if self._length is not None:
603
+ return self._length
604
+
605
+ try:
606
+ self._length = len(self._iterable) # type: ignore
607
+ except TypeError:
608
+ iterable = [x async for x in self._iterator]
609
+ self._iterator = self._to_iterator(iterable)
610
+ self._length = len(iterable) + self.index + (self._after is not missing)
611
+
612
+ return self._length
613
+
614
+ @property
615
+ async def revindex0(self) -> int: # type: ignore
616
+ return await self.length - self.index
617
+
618
+ @property
619
+ async def revindex(self) -> int: # type: ignore
620
+ return await self.length - self.index0
621
+
622
+ async def _peek_next(self) -> t.Any:
623
+ if self._after is not missing:
624
+ return self._after
625
+
626
+ try:
627
+ self._after = await self._iterator.__anext__()
628
+ except StopAsyncIteration:
629
+ self._after = missing
630
+
631
+ return self._after
632
+
633
+ @property
634
+ async def last(self) -> bool: # type: ignore
635
+ return await self._peek_next() is missing
636
+
637
+ @property
638
+ async def nextitem(self) -> t.Union[t.Any, "Undefined"]:
639
+ rv = await self._peek_next()
640
+
641
+ if rv is missing:
642
+ return self._undefined("there is no next item")
643
+
644
+ return rv
645
+
646
+ def __aiter__(self) -> "AsyncLoopContext":
647
+ return self
648
+
649
+ async def __anext__(self) -> t.Tuple[t.Any, "AsyncLoopContext"]:
650
+ if self._after is not missing:
651
+ rv = self._after
652
+ self._after = missing
653
+ else:
654
+ rv = await self._iterator.__anext__()
655
+
656
+ self.index0 += 1
657
+ self._before = self._current
658
+ self._current = rv
659
+ return rv, self
660
+
661
+
662
+ class Macro:
663
+ """Wraps a macro function."""
664
+
665
+ def __init__(
666
+ self,
667
+ environment: "Environment",
668
+ func: t.Callable[..., str],
669
+ name: str,
670
+ arguments: t.List[str],
671
+ catch_kwargs: bool,
672
+ catch_varargs: bool,
673
+ caller: bool,
674
+ default_autoescape: t.Optional[bool] = None,
675
+ ):
676
+ self._environment = environment
677
+ self._func = func
678
+ self._argument_count = len(arguments)
679
+ self.name = name
680
+ self.arguments = arguments
681
+ self.catch_kwargs = catch_kwargs
682
+ self.catch_varargs = catch_varargs
683
+ self.caller = caller
684
+ self.explicit_caller = "caller" in arguments
685
+
686
+ if default_autoescape is None:
687
+ if callable(environment.autoescape):
688
+ default_autoescape = environment.autoescape(None)
689
+ else:
690
+ default_autoescape = environment.autoescape
691
+
692
+ self._default_autoescape = default_autoescape
693
+
694
+ @internalcode
695
+ @pass_eval_context
696
+ def __call__(self, *args: t.Any, **kwargs: t.Any) -> str:
697
+ # This requires a bit of explanation, In the past we used to
698
+ # decide largely based on compile-time information if a macro is
699
+ # safe or unsafe. While there was a volatile mode it was largely
700
+ # unused for deciding on escaping. This turns out to be
701
+ # problematic for macros because whether a macro is safe depends not
702
+ # on the escape mode when it was defined, but rather when it was used.
703
+ #
704
+ # Because however we export macros from the module system and
705
+ # there are historic callers that do not pass an eval context (and
706
+ # will continue to not pass one), we need to perform an instance
707
+ # check here.
708
+ #
709
+ # This is considered safe because an eval context is not a valid
710
+ # argument to callables otherwise anyway. Worst case here is
711
+ # that if no eval context is passed we fall back to the compile
712
+ # time autoescape flag.
713
+ if args and isinstance(args[0], EvalContext):
714
+ autoescape = args[0].autoescape
715
+ args = args[1:]
716
+ else:
717
+ autoescape = self._default_autoescape
718
+
719
+ # try to consume the positional arguments
720
+ arguments = list(args[: self._argument_count])
721
+ off = len(arguments)
722
+
723
+ # For information why this is necessary refer to the handling
724
+ # of caller in the `macro_body` handler in the compiler.
725
+ found_caller = False
726
+
727
+ # if the number of arguments consumed is not the number of
728
+ # arguments expected we start filling in keyword arguments
729
+ # and defaults.
730
+ if off != self._argument_count:
731
+ for name in self.arguments[len(arguments) :]:
732
+ try:
733
+ value = kwargs.pop(name)
734
+ except KeyError:
735
+ value = missing
736
+ if name == "caller":
737
+ found_caller = True
738
+ arguments.append(value)
739
+ else:
740
+ found_caller = self.explicit_caller
741
+
742
+ # it's important that the order of these arguments does not change
743
+ # if not also changed in the compiler's `function_scoping` method.
744
+ # the order is caller, keyword arguments, positional arguments!
745
+ if self.caller and not found_caller:
746
+ caller = kwargs.pop("caller", None)
747
+ if caller is None:
748
+ caller = self._environment.undefined("No caller defined", name="caller")
749
+ arguments.append(caller)
750
+
751
+ if self.catch_kwargs:
752
+ arguments.append(kwargs)
753
+ elif kwargs:
754
+ if "caller" in kwargs:
755
+ raise TypeError(
756
+ f"macro {self.name!r} was invoked with two values for the special"
757
+ " caller argument. This is most likely a bug."
758
+ )
759
+ raise TypeError(
760
+ f"macro {self.name!r} takes no keyword argument {next(iter(kwargs))!r}"
761
+ )
762
+ if self.catch_varargs:
763
+ arguments.append(args[self._argument_count :])
764
+ elif len(args) > self._argument_count:
765
+ raise TypeError(
766
+ f"macro {self.name!r} takes not more than"
767
+ f" {len(self.arguments)} argument(s)"
768
+ )
769
+
770
+ return self._invoke(arguments, autoescape)
771
+
772
+ async def _async_invoke(self, arguments: t.List[t.Any], autoescape: bool) -> str:
773
+ rv = await self._func(*arguments) # type: ignore
774
+
775
+ if autoescape:
776
+ return Markup(rv)
777
+
778
+ return rv # type: ignore
779
+
780
+ def _invoke(self, arguments: t.List[t.Any], autoescape: bool) -> str:
781
+ if self._environment.is_async:
782
+ return self._async_invoke(arguments, autoescape) # type: ignore
783
+
784
+ rv = self._func(*arguments)
785
+
786
+ if autoescape:
787
+ rv = Markup(rv)
788
+
789
+ return rv
790
+
791
+ def __repr__(self) -> str:
792
+ name = "anonymous" if self.name is None else repr(self.name)
793
+ return f"<{type(self).__name__} {name}>"
794
+
795
+
796
+ class Undefined:
797
+ """The default undefined type. This can be printed, iterated, and treated as
798
+ a boolean. Any other operation will raise an :exc:`UndefinedError`.
799
+
800
+ >>> foo = Undefined(name='foo')
801
+ >>> str(foo)
802
+ ''
803
+ >>> not foo
804
+ True
805
+ >>> foo + 42
806
+ Traceback (most recent call last):
807
+ ...
808
+ jinja2.exceptions.UndefinedError: 'foo' is undefined
809
+ """
810
+
811
+ __slots__ = (
812
+ "_undefined_hint",
813
+ "_undefined_obj",
814
+ "_undefined_name",
815
+ "_undefined_exception",
816
+ )
817
+
818
+ def __init__(
819
+ self,
820
+ hint: t.Optional[str] = None,
821
+ obj: t.Any = missing,
822
+ name: t.Optional[str] = None,
823
+ exc: t.Type[TemplateRuntimeError] = UndefinedError,
824
+ ) -> None:
825
+ self._undefined_hint = hint
826
+ self._undefined_obj = obj
827
+ self._undefined_name = name
828
+ self._undefined_exception = exc
829
+
830
+ @property
831
+ def _undefined_message(self) -> str:
832
+ """Build a message about the undefined value based on how it was
833
+ accessed.
834
+ """
835
+ if self._undefined_hint:
836
+ return self._undefined_hint
837
+
838
+ if self._undefined_obj is missing:
839
+ return f"{self._undefined_name!r} is undefined"
840
+
841
+ if not isinstance(self._undefined_name, str):
842
+ return (
843
+ f"{object_type_repr(self._undefined_obj)} has no"
844
+ f" element {self._undefined_name!r}"
845
+ )
846
+
847
+ return (
848
+ f"{object_type_repr(self._undefined_obj)!r} has no"
849
+ f" attribute {self._undefined_name!r}"
850
+ )
851
+
852
+ @internalcode
853
+ def _fail_with_undefined_error(
854
+ self, *args: t.Any, **kwargs: t.Any
855
+ ) -> "te.NoReturn":
856
+ """Raise an :exc:`UndefinedError` when operations are performed
857
+ on the undefined value.
858
+ """
859
+ raise self._undefined_exception(self._undefined_message)
860
+
861
+ @internalcode
862
+ def __getattr__(self, name: str) -> t.Any:
863
+ # Raise AttributeError on requests for names that appear to be unimplemented
864
+ # dunder methods to keep Python's internal protocol probing behaviors working
865
+ # properly in cases where another exception type could cause unexpected or
866
+ # difficult-to-diagnose failures.
867
+ if name[:2] == "__" and name[-2:] == "__":
868
+ raise AttributeError(name)
869
+
870
+ return self._fail_with_undefined_error()
871
+
872
+ __add__ = __radd__ = __sub__ = __rsub__ = _fail_with_undefined_error
873
+ __mul__ = __rmul__ = __div__ = __rdiv__ = _fail_with_undefined_error
874
+ __truediv__ = __rtruediv__ = _fail_with_undefined_error
875
+ __floordiv__ = __rfloordiv__ = _fail_with_undefined_error
876
+ __mod__ = __rmod__ = _fail_with_undefined_error
877
+ __pos__ = __neg__ = _fail_with_undefined_error
878
+ __call__ = __getitem__ = _fail_with_undefined_error
879
+ __lt__ = __le__ = __gt__ = __ge__ = _fail_with_undefined_error
880
+ __int__ = __float__ = __complex__ = _fail_with_undefined_error
881
+ __pow__ = __rpow__ = _fail_with_undefined_error
882
+
883
+ def __eq__(self, other: t.Any) -> bool:
884
+ return type(self) is type(other)
885
+
886
+ def __ne__(self, other: t.Any) -> bool:
887
+ return not self.__eq__(other)
888
+
889
+ def __hash__(self) -> int:
890
+ return id(type(self))
891
+
892
+ def __str__(self) -> str:
893
+ return ""
894
+
895
+ def __len__(self) -> int:
896
+ return 0
897
+
898
+ def __iter__(self) -> t.Iterator[t.Any]:
899
+ yield from ()
900
+
901
+ async def __aiter__(self) -> t.AsyncIterator[t.Any]:
902
+ for _ in ():
903
+ yield
904
+
905
+ def __bool__(self) -> bool:
906
+ return False
907
+
908
+ def __repr__(self) -> str:
909
+ return "Undefined"
910
+
911
+
912
+ def make_logging_undefined(
913
+ logger: t.Optional["logging.Logger"] = None, base: t.Type[Undefined] = Undefined
914
+ ) -> t.Type[Undefined]:
915
+ """Given a logger object this returns a new undefined class that will
916
+ log certain failures. It will log iterations and printing. If no
917
+ logger is given a default logger is created.
918
+
919
+ Example::
920
+
921
+ logger = logging.getLogger(__name__)
922
+ LoggingUndefined = make_logging_undefined(
923
+ logger=logger,
924
+ base=Undefined
925
+ )
926
+
927
+ .. versionadded:: 2.8
928
+
929
+ :param logger: the logger to use. If not provided, a default logger
930
+ is created.
931
+ :param base: the base class to add logging functionality to. This
932
+ defaults to :class:`Undefined`.
933
+ """
934
+ if logger is None:
935
+ import logging
936
+
937
+ logger = logging.getLogger(__name__)
938
+ logger.addHandler(logging.StreamHandler(sys.stderr))
939
+
940
+ def _log_message(undef: Undefined) -> None:
941
+ logger.warning("Template variable warning: %s", undef._undefined_message)
942
+
943
+ class LoggingUndefined(base): # type: ignore
944
+ __slots__ = ()
945
+
946
+ def _fail_with_undefined_error( # type: ignore
947
+ self, *args: t.Any, **kwargs: t.Any
948
+ ) -> "te.NoReturn":
949
+ try:
950
+ super()._fail_with_undefined_error(*args, **kwargs)
951
+ except self._undefined_exception as e:
952
+ logger.error("Template variable error: %s", e) # type: ignore
953
+ raise e
954
+
955
+ def __str__(self) -> str:
956
+ _log_message(self)
957
+ return super().__str__() # type: ignore
958
+
959
+ def __iter__(self) -> t.Iterator[t.Any]:
960
+ _log_message(self)
961
+ return super().__iter__() # type: ignore
962
+
963
+ def __bool__(self) -> bool:
964
+ _log_message(self)
965
+ return super().__bool__() # type: ignore
966
+
967
+ return LoggingUndefined
968
+
969
+
970
+ class ChainableUndefined(Undefined):
971
+ """An undefined that is chainable, where both ``__getattr__`` and
972
+ ``__getitem__`` return itself rather than raising an
973
+ :exc:`UndefinedError`.
974
+
975
+ >>> foo = ChainableUndefined(name='foo')
976
+ >>> str(foo.bar['baz'])
977
+ ''
978
+ >>> foo.bar['baz'] + 42
979
+ Traceback (most recent call last):
980
+ ...
981
+ jinja2.exceptions.UndefinedError: 'foo' is undefined
982
+
983
+ .. versionadded:: 2.11.0
984
+ """
985
+
986
+ __slots__ = ()
987
+
988
+ def __html__(self) -> str:
989
+ return str(self)
990
+
991
+ def __getattr__(self, name: str) -> "ChainableUndefined":
992
+ # Raise AttributeError on requests for names that appear to be unimplemented
993
+ # dunder methods to avoid confusing Python with truthy non-method objects that
994
+ # do not implement the protocol being probed for. e.g., copy.copy(Undefined())
995
+ # fails spectacularly if getattr(Undefined(), '__setstate__') returns an
996
+ # Undefined object instead of raising AttributeError to signal that it does not
997
+ # support that style of object initialization.
998
+ if name[:2] == "__" and name[-2:] == "__":
999
+ raise AttributeError(name)
1000
+
1001
+ return self
1002
+
1003
+ def __getitem__(self, _name: str) -> "ChainableUndefined": # type: ignore[override]
1004
+ return self
1005
+
1006
+
1007
+ class DebugUndefined(Undefined):
1008
+ """An undefined that returns the debug info when printed.
1009
+
1010
+ >>> foo = DebugUndefined(name='foo')
1011
+ >>> str(foo)
1012
+ '{{ foo }}'
1013
+ >>> not foo
1014
+ True
1015
+ >>> foo + 42
1016
+ Traceback (most recent call last):
1017
+ ...
1018
+ jinja2.exceptions.UndefinedError: 'foo' is undefined
1019
+ """
1020
+
1021
+ __slots__ = ()
1022
+
1023
+ def __str__(self) -> str:
1024
+ if self._undefined_hint:
1025
+ message = f"undefined value printed: {self._undefined_hint}"
1026
+
1027
+ elif self._undefined_obj is missing:
1028
+ message = self._undefined_name # type: ignore
1029
+
1030
+ else:
1031
+ message = (
1032
+ f"no such element: {object_type_repr(self._undefined_obj)}"
1033
+ f"[{self._undefined_name!r}]"
1034
+ )
1035
+
1036
+ return f"{{{{ {message} }}}}"
1037
+
1038
+
1039
+ class StrictUndefined(Undefined):
1040
+ """An undefined that barks on print and iteration as well as boolean
1041
+ tests and all kinds of comparisons. In other words: you can do nothing
1042
+ with it except checking if it's defined using the `defined` test.
1043
+
1044
+ >>> foo = StrictUndefined(name='foo')
1045
+ >>> str(foo)
1046
+ Traceback (most recent call last):
1047
+ ...
1048
+ jinja2.exceptions.UndefinedError: 'foo' is undefined
1049
+ >>> not foo
1050
+ Traceback (most recent call last):
1051
+ ...
1052
+ jinja2.exceptions.UndefinedError: 'foo' is undefined
1053
+ >>> foo + 42
1054
+ Traceback (most recent call last):
1055
+ ...
1056
+ jinja2.exceptions.UndefinedError: 'foo' is undefined
1057
+ """
1058
+
1059
+ __slots__ = ()
1060
+ __iter__ = __str__ = __len__ = Undefined._fail_with_undefined_error
1061
+ __eq__ = __ne__ = __bool__ = __hash__ = Undefined._fail_with_undefined_error
1062
+ __contains__ = Undefined._fail_with_undefined_error