okstra 0.31.0 → 0.32.1

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 (52) 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_ctl/wizard.py +16 -5
  12. package/runtime/python/okstra_token_usage/__init__.py +5 -1
  13. package/runtime/python/okstra_token_usage/cli.py +66 -36
  14. package/runtime/python/okstra_token_usage/report.py +148 -65
  15. package/runtime/python/okstra_vendor/__init__.py +37 -0
  16. package/runtime/python/okstra_vendor/jinja2/__init__.py +38 -0
  17. package/runtime/python/okstra_vendor/jinja2/_identifier.py +6 -0
  18. package/runtime/python/okstra_vendor/jinja2/async_utils.py +99 -0
  19. package/runtime/python/okstra_vendor/jinja2/bccache.py +408 -0
  20. package/runtime/python/okstra_vendor/jinja2/compiler.py +1998 -0
  21. package/runtime/python/okstra_vendor/jinja2/constants.py +20 -0
  22. package/runtime/python/okstra_vendor/jinja2/debug.py +191 -0
  23. package/runtime/python/okstra_vendor/jinja2/defaults.py +48 -0
  24. package/runtime/python/okstra_vendor/jinja2/environment.py +1672 -0
  25. package/runtime/python/okstra_vendor/jinja2/exceptions.py +166 -0
  26. package/runtime/python/okstra_vendor/jinja2/ext.py +870 -0
  27. package/runtime/python/okstra_vendor/jinja2/filters.py +1873 -0
  28. package/runtime/python/okstra_vendor/jinja2/idtracking.py +318 -0
  29. package/runtime/python/okstra_vendor/jinja2/lexer.py +868 -0
  30. package/runtime/python/okstra_vendor/jinja2/loaders.py +693 -0
  31. package/runtime/python/okstra_vendor/jinja2/meta.py +112 -0
  32. package/runtime/python/okstra_vendor/jinja2/nativetypes.py +130 -0
  33. package/runtime/python/okstra_vendor/jinja2/nodes.py +1206 -0
  34. package/runtime/python/okstra_vendor/jinja2/optimizer.py +48 -0
  35. package/runtime/python/okstra_vendor/jinja2/parser.py +1049 -0
  36. package/runtime/python/okstra_vendor/jinja2/py.typed +0 -0
  37. package/runtime/python/okstra_vendor/jinja2/runtime.py +1062 -0
  38. package/runtime/python/okstra_vendor/jinja2/sandbox.py +436 -0
  39. package/runtime/python/okstra_vendor/jinja2/tests.py +256 -0
  40. package/runtime/python/okstra_vendor/jinja2/utils.py +766 -0
  41. package/runtime/python/okstra_vendor/jinja2/visitor.py +92 -0
  42. package/runtime/python/okstra_vendor/markupsafe/__init__.py +396 -0
  43. package/runtime/python/okstra_vendor/markupsafe/_native.py +8 -0
  44. package/runtime/python/okstra_vendor/markupsafe/py.typed +0 -0
  45. package/runtime/schemas/final-report-v1.0.schema.json +1391 -0
  46. package/runtime/skills/okstra-report-writer/SKILL.md +29 -28
  47. package/runtime/templates/reports/final-report.template.md +370 -411
  48. package/runtime/templates/reports/report.css +12 -6
  49. package/runtime/validators/lib/fixtures.sh +7 -7
  50. package/runtime/validators/validate-report-views.py +24 -153
  51. package/runtime/validators/validate-run.py +102 -19
  52. package/src/install.mjs +20 -1
@@ -0,0 +1,436 @@
1
+ """A sandbox layer that ensures unsafe operations cannot be performed.
2
+ Useful when the template itself comes from an untrusted source.
3
+ """
4
+
5
+ import operator
6
+ import types
7
+ import typing as t
8
+ from _string import formatter_field_name_split # type: ignore
9
+ from collections import abc
10
+ from collections import deque
11
+ from functools import update_wrapper
12
+ from string import Formatter
13
+
14
+ from markupsafe import EscapeFormatter
15
+ from markupsafe import Markup
16
+
17
+ from .environment import Environment
18
+ from .exceptions import SecurityError
19
+ from .runtime import Context
20
+ from .runtime import Undefined
21
+
22
+ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
23
+
24
+ #: maximum number of items a range may produce
25
+ MAX_RANGE = 100000
26
+
27
+ #: Unsafe function attributes.
28
+ UNSAFE_FUNCTION_ATTRIBUTES: t.Set[str] = set()
29
+
30
+ #: Unsafe method attributes. Function attributes are unsafe for methods too.
31
+ UNSAFE_METHOD_ATTRIBUTES: t.Set[str] = set()
32
+
33
+ #: unsafe generator attributes.
34
+ UNSAFE_GENERATOR_ATTRIBUTES = {"gi_frame", "gi_code"}
35
+
36
+ #: unsafe attributes on coroutines
37
+ UNSAFE_COROUTINE_ATTRIBUTES = {"cr_frame", "cr_code"}
38
+
39
+ #: unsafe attributes on async generators
40
+ UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = {"ag_code", "ag_frame"}
41
+
42
+ _mutable_spec: t.Tuple[t.Tuple[t.Type[t.Any], t.FrozenSet[str]], ...] = (
43
+ (
44
+ abc.MutableSet,
45
+ frozenset(
46
+ [
47
+ "add",
48
+ "clear",
49
+ "difference_update",
50
+ "discard",
51
+ "pop",
52
+ "remove",
53
+ "symmetric_difference_update",
54
+ "update",
55
+ ]
56
+ ),
57
+ ),
58
+ (
59
+ abc.MutableMapping,
60
+ frozenset(["clear", "pop", "popitem", "setdefault", "update"]),
61
+ ),
62
+ (
63
+ abc.MutableSequence,
64
+ frozenset(
65
+ ["append", "clear", "pop", "reverse", "insert", "sort", "extend", "remove"]
66
+ ),
67
+ ),
68
+ (
69
+ deque,
70
+ frozenset(
71
+ [
72
+ "append",
73
+ "appendleft",
74
+ "clear",
75
+ "extend",
76
+ "extendleft",
77
+ "pop",
78
+ "popleft",
79
+ "remove",
80
+ "rotate",
81
+ ]
82
+ ),
83
+ ),
84
+ )
85
+
86
+
87
+ def safe_range(*args: int) -> range:
88
+ """A range that can't generate ranges with a length of more than
89
+ MAX_RANGE items.
90
+ """
91
+ rng = range(*args)
92
+
93
+ if len(rng) > MAX_RANGE:
94
+ raise OverflowError(
95
+ "Range too big. The sandbox blocks ranges larger than"
96
+ f" MAX_RANGE ({MAX_RANGE})."
97
+ )
98
+
99
+ return rng
100
+
101
+
102
+ def unsafe(f: F) -> F:
103
+ """Marks a function or method as unsafe.
104
+
105
+ .. code-block: python
106
+
107
+ @unsafe
108
+ def delete(self):
109
+ pass
110
+ """
111
+ f.unsafe_callable = True # type: ignore
112
+ return f
113
+
114
+
115
+ def is_internal_attribute(obj: t.Any, attr: str) -> bool:
116
+ """Test if the attribute given is an internal python attribute. For
117
+ example this function returns `True` for the `func_code` attribute of
118
+ python objects. This is useful if the environment method
119
+ :meth:`~SandboxedEnvironment.is_safe_attribute` is overridden.
120
+
121
+ >>> from jinja2.sandbox import is_internal_attribute
122
+ >>> is_internal_attribute(str, "mro")
123
+ True
124
+ >>> is_internal_attribute(str, "upper")
125
+ False
126
+ """
127
+ if isinstance(obj, types.FunctionType):
128
+ if attr in UNSAFE_FUNCTION_ATTRIBUTES:
129
+ return True
130
+ elif isinstance(obj, types.MethodType):
131
+ if attr in UNSAFE_FUNCTION_ATTRIBUTES or attr in UNSAFE_METHOD_ATTRIBUTES:
132
+ return True
133
+ elif isinstance(obj, type):
134
+ if attr == "mro":
135
+ return True
136
+ elif isinstance(obj, (types.CodeType, types.TracebackType, types.FrameType)):
137
+ return True
138
+ elif isinstance(obj, types.GeneratorType):
139
+ if attr in UNSAFE_GENERATOR_ATTRIBUTES:
140
+ return True
141
+ elif hasattr(types, "CoroutineType") and isinstance(obj, types.CoroutineType):
142
+ if attr in UNSAFE_COROUTINE_ATTRIBUTES:
143
+ return True
144
+ elif hasattr(types, "AsyncGeneratorType") and isinstance(
145
+ obj, types.AsyncGeneratorType
146
+ ):
147
+ if attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES:
148
+ return True
149
+ return attr.startswith("__")
150
+
151
+
152
+ def modifies_known_mutable(obj: t.Any, attr: str) -> bool:
153
+ """This function checks if an attribute on a builtin mutable object
154
+ (list, dict, set or deque) or the corresponding ABCs would modify it
155
+ if called.
156
+
157
+ >>> modifies_known_mutable({}, "clear")
158
+ True
159
+ >>> modifies_known_mutable({}, "keys")
160
+ False
161
+ >>> modifies_known_mutable([], "append")
162
+ True
163
+ >>> modifies_known_mutable([], "index")
164
+ False
165
+
166
+ If called with an unsupported object, ``False`` is returned.
167
+
168
+ >>> modifies_known_mutable("foo", "upper")
169
+ False
170
+ """
171
+ for typespec, unsafe in _mutable_spec:
172
+ if isinstance(obj, typespec):
173
+ return attr in unsafe
174
+ return False
175
+
176
+
177
+ class SandboxedEnvironment(Environment):
178
+ """The sandboxed environment. It works like the regular environment but
179
+ tells the compiler to generate sandboxed code. Additionally subclasses of
180
+ this environment may override the methods that tell the runtime what
181
+ attributes or functions are safe to access.
182
+
183
+ If the template tries to access insecure code a :exc:`SecurityError` is
184
+ raised. However also other exceptions may occur during the rendering so
185
+ the caller has to ensure that all exceptions are caught.
186
+ """
187
+
188
+ sandboxed = True
189
+
190
+ #: default callback table for the binary operators. A copy of this is
191
+ #: available on each instance of a sandboxed environment as
192
+ #: :attr:`binop_table`
193
+ default_binop_table: t.Dict[str, t.Callable[[t.Any, t.Any], t.Any]] = {
194
+ "+": operator.add,
195
+ "-": operator.sub,
196
+ "*": operator.mul,
197
+ "/": operator.truediv,
198
+ "//": operator.floordiv,
199
+ "**": operator.pow,
200
+ "%": operator.mod,
201
+ }
202
+
203
+ #: default callback table for the unary operators. A copy of this is
204
+ #: available on each instance of a sandboxed environment as
205
+ #: :attr:`unop_table`
206
+ default_unop_table: t.Dict[str, t.Callable[[t.Any], t.Any]] = {
207
+ "+": operator.pos,
208
+ "-": operator.neg,
209
+ }
210
+
211
+ #: a set of binary operators that should be intercepted. Each operator
212
+ #: that is added to this set (empty by default) is delegated to the
213
+ #: :meth:`call_binop` method that will perform the operator. The default
214
+ #: operator callback is specified by :attr:`binop_table`.
215
+ #:
216
+ #: The following binary operators are interceptable:
217
+ #: ``//``, ``%``, ``+``, ``*``, ``-``, ``/``, and ``**``
218
+ #:
219
+ #: The default operation form the operator table corresponds to the
220
+ #: builtin function. Intercepted calls are always slower than the native
221
+ #: operator call, so make sure only to intercept the ones you are
222
+ #: interested in.
223
+ #:
224
+ #: .. versionadded:: 2.6
225
+ intercepted_binops: t.FrozenSet[str] = frozenset()
226
+
227
+ #: a set of unary operators that should be intercepted. Each operator
228
+ #: that is added to this set (empty by default) is delegated to the
229
+ #: :meth:`call_unop` method that will perform the operator. The default
230
+ #: operator callback is specified by :attr:`unop_table`.
231
+ #:
232
+ #: The following unary operators are interceptable: ``+``, ``-``
233
+ #:
234
+ #: The default operation form the operator table corresponds to the
235
+ #: builtin function. Intercepted calls are always slower than the native
236
+ #: operator call, so make sure only to intercept the ones you are
237
+ #: interested in.
238
+ #:
239
+ #: .. versionadded:: 2.6
240
+ intercepted_unops: t.FrozenSet[str] = frozenset()
241
+
242
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
243
+ super().__init__(*args, **kwargs)
244
+ self.globals["range"] = safe_range
245
+ self.binop_table = self.default_binop_table.copy()
246
+ self.unop_table = self.default_unop_table.copy()
247
+
248
+ def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
249
+ """The sandboxed environment will call this method to check if the
250
+ attribute of an object is safe to access. Per default all attributes
251
+ starting with an underscore are considered private as well as the
252
+ special attributes of internal python objects as returned by the
253
+ :func:`is_internal_attribute` function.
254
+ """
255
+ return not (attr.startswith("_") or is_internal_attribute(obj, attr))
256
+
257
+ def is_safe_callable(self, obj: t.Any) -> bool:
258
+ """Check if an object is safely callable. By default callables
259
+ are considered safe unless decorated with :func:`unsafe`.
260
+
261
+ This also recognizes the Django convention of setting
262
+ ``func.alters_data = True``.
263
+ """
264
+ return not (
265
+ getattr(obj, "unsafe_callable", False) or getattr(obj, "alters_data", False)
266
+ )
267
+
268
+ def call_binop(
269
+ self, context: Context, operator: str, left: t.Any, right: t.Any
270
+ ) -> t.Any:
271
+ """For intercepted binary operator calls (:meth:`intercepted_binops`)
272
+ this function is executed instead of the builtin operator. This can
273
+ be used to fine tune the behavior of certain operators.
274
+
275
+ .. versionadded:: 2.6
276
+ """
277
+ return self.binop_table[operator](left, right)
278
+
279
+ def call_unop(self, context: Context, operator: str, arg: t.Any) -> t.Any:
280
+ """For intercepted unary operator calls (:meth:`intercepted_unops`)
281
+ this function is executed instead of the builtin operator. This can
282
+ be used to fine tune the behavior of certain operators.
283
+
284
+ .. versionadded:: 2.6
285
+ """
286
+ return self.unop_table[operator](arg)
287
+
288
+ def getitem(
289
+ self, obj: t.Any, argument: t.Union[str, t.Any]
290
+ ) -> t.Union[t.Any, Undefined]:
291
+ """Subscribe an object from sandboxed code."""
292
+ try:
293
+ return obj[argument]
294
+ except (TypeError, LookupError):
295
+ if isinstance(argument, str):
296
+ try:
297
+ attr = str(argument)
298
+ except Exception:
299
+ pass
300
+ else:
301
+ try:
302
+ value = getattr(obj, attr)
303
+ except AttributeError:
304
+ pass
305
+ else:
306
+ fmt = self.wrap_str_format(value)
307
+ if fmt is not None:
308
+ return fmt
309
+ if self.is_safe_attribute(obj, argument, value):
310
+ return value
311
+ return self.unsafe_undefined(obj, argument)
312
+ return self.undefined(obj=obj, name=argument)
313
+
314
+ def getattr(self, obj: t.Any, attribute: str) -> t.Union[t.Any, Undefined]:
315
+ """Subscribe an object from sandboxed code and prefer the
316
+ attribute. The attribute passed *must* be a bytestring.
317
+ """
318
+ try:
319
+ value = getattr(obj, attribute)
320
+ except AttributeError:
321
+ try:
322
+ return obj[attribute]
323
+ except (TypeError, LookupError):
324
+ pass
325
+ else:
326
+ fmt = self.wrap_str_format(value)
327
+ if fmt is not None:
328
+ return fmt
329
+ if self.is_safe_attribute(obj, attribute, value):
330
+ return value
331
+ return self.unsafe_undefined(obj, attribute)
332
+ return self.undefined(obj=obj, name=attribute)
333
+
334
+ def unsafe_undefined(self, obj: t.Any, attribute: str) -> Undefined:
335
+ """Return an undefined object for unsafe attributes."""
336
+ return self.undefined(
337
+ f"access to attribute {attribute!r} of"
338
+ f" {type(obj).__name__!r} object is unsafe.",
339
+ name=attribute,
340
+ obj=obj,
341
+ exc=SecurityError,
342
+ )
343
+
344
+ def wrap_str_format(self, value: t.Any) -> t.Optional[t.Callable[..., str]]:
345
+ """If the given value is a ``str.format`` or ``str.format_map`` method,
346
+ return a new function than handles sandboxing. This is done at access
347
+ rather than in :meth:`call`, so that calls made without ``call`` are
348
+ also sandboxed.
349
+ """
350
+ if not isinstance(
351
+ value, (types.MethodType, types.BuiltinMethodType)
352
+ ) or value.__name__ not in ("format", "format_map"):
353
+ return None
354
+
355
+ f_self: t.Any = value.__self__
356
+
357
+ if not isinstance(f_self, str):
358
+ return None
359
+
360
+ str_type: t.Type[str] = type(f_self)
361
+ is_format_map = value.__name__ == "format_map"
362
+ formatter: SandboxedFormatter
363
+
364
+ if isinstance(f_self, Markup):
365
+ formatter = SandboxedEscapeFormatter(self, escape=f_self.escape)
366
+ else:
367
+ formatter = SandboxedFormatter(self)
368
+
369
+ vformat = formatter.vformat
370
+
371
+ def wrapper(*args: t.Any, **kwargs: t.Any) -> str:
372
+ if is_format_map:
373
+ if kwargs:
374
+ raise TypeError("format_map() takes no keyword arguments")
375
+
376
+ if len(args) != 1:
377
+ raise TypeError(
378
+ f"format_map() takes exactly one argument ({len(args)} given)"
379
+ )
380
+
381
+ kwargs = args[0]
382
+ args = ()
383
+
384
+ return str_type(vformat(f_self, args, kwargs))
385
+
386
+ return update_wrapper(wrapper, value)
387
+
388
+ def call(
389
+ __self, # noqa: B902
390
+ __context: Context,
391
+ __obj: t.Any,
392
+ *args: t.Any,
393
+ **kwargs: t.Any,
394
+ ) -> t.Any:
395
+ """Call an object from sandboxed code."""
396
+
397
+ # the double prefixes are to avoid double keyword argument
398
+ # errors when proxying the call.
399
+ if not __self.is_safe_callable(__obj):
400
+ raise SecurityError(f"{__obj!r} is not safely callable")
401
+ return __context.call(__obj, *args, **kwargs)
402
+
403
+
404
+ class ImmutableSandboxedEnvironment(SandboxedEnvironment):
405
+ """Works exactly like the regular `SandboxedEnvironment` but does not
406
+ permit modifications on the builtin mutable objects `list`, `set`, and
407
+ `dict` by using the :func:`modifies_known_mutable` function.
408
+ """
409
+
410
+ def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
411
+ if not super().is_safe_attribute(obj, attr, value):
412
+ return False
413
+
414
+ return not modifies_known_mutable(obj, attr)
415
+
416
+
417
+ class SandboxedFormatter(Formatter):
418
+ def __init__(self, env: Environment, **kwargs: t.Any) -> None:
419
+ self._env = env
420
+ super().__init__(**kwargs)
421
+
422
+ def get_field(
423
+ self, field_name: str, args: t.Sequence[t.Any], kwargs: t.Mapping[str, t.Any]
424
+ ) -> t.Tuple[t.Any, str]:
425
+ first, rest = formatter_field_name_split(field_name)
426
+ obj = self.get_value(first, args, kwargs)
427
+ for is_attr, i in rest:
428
+ if is_attr:
429
+ obj = self._env.getattr(obj, i)
430
+ else:
431
+ obj = self._env.getitem(obj, i)
432
+ return obj, first
433
+
434
+
435
+ class SandboxedEscapeFormatter(SandboxedFormatter, EscapeFormatter):
436
+ pass
@@ -0,0 +1,256 @@
1
+ """Built-in template tests used with the ``is`` operator."""
2
+
3
+ import operator
4
+ import typing as t
5
+ from collections import abc
6
+ from numbers import Number
7
+
8
+ from .runtime import Undefined
9
+ from .utils import pass_environment
10
+
11
+ if t.TYPE_CHECKING:
12
+ from .environment import Environment
13
+
14
+
15
+ def test_odd(value: int) -> bool:
16
+ """Return true if the variable is odd."""
17
+ return value % 2 == 1
18
+
19
+
20
+ def test_even(value: int) -> bool:
21
+ """Return true if the variable is even."""
22
+ return value % 2 == 0
23
+
24
+
25
+ def test_divisibleby(value: int, num: int) -> bool:
26
+ """Check if a variable is divisible by a number."""
27
+ return value % num == 0
28
+
29
+
30
+ def test_defined(value: t.Any) -> bool:
31
+ """Return true if the variable is defined:
32
+
33
+ .. sourcecode:: jinja
34
+
35
+ {% if variable is defined %}
36
+ value of variable: {{ variable }}
37
+ {% else %}
38
+ variable is not defined
39
+ {% endif %}
40
+
41
+ See the :func:`default` filter for a simple way to set undefined
42
+ variables.
43
+ """
44
+ return not isinstance(value, Undefined)
45
+
46
+
47
+ def test_undefined(value: t.Any) -> bool:
48
+ """Like :func:`defined` but the other way round."""
49
+ return isinstance(value, Undefined)
50
+
51
+
52
+ @pass_environment
53
+ def test_filter(env: "Environment", value: str) -> bool:
54
+ """Check if a filter exists by name. Useful if a filter may be
55
+ optionally available.
56
+
57
+ .. code-block:: jinja
58
+
59
+ {% if 'markdown' is filter %}
60
+ {{ value | markdown }}
61
+ {% else %}
62
+ {{ value }}
63
+ {% endif %}
64
+
65
+ .. versionadded:: 3.0
66
+ """
67
+ return value in env.filters
68
+
69
+
70
+ @pass_environment
71
+ def test_test(env: "Environment", value: str) -> bool:
72
+ """Check if a test exists by name. Useful if a test may be
73
+ optionally available.
74
+
75
+ .. code-block:: jinja
76
+
77
+ {% if 'loud' is test %}
78
+ {% if value is loud %}
79
+ {{ value|upper }}
80
+ {% else %}
81
+ {{ value|lower }}
82
+ {% endif %}
83
+ {% else %}
84
+ {{ value }}
85
+ {% endif %}
86
+
87
+ .. versionadded:: 3.0
88
+ """
89
+ return value in env.tests
90
+
91
+
92
+ def test_none(value: t.Any) -> bool:
93
+ """Return true if the variable is none."""
94
+ return value is None
95
+
96
+
97
+ def test_boolean(value: t.Any) -> bool:
98
+ """Return true if the object is a boolean value.
99
+
100
+ .. versionadded:: 2.11
101
+ """
102
+ return value is True or value is False
103
+
104
+
105
+ def test_false(value: t.Any) -> bool:
106
+ """Return true if the object is False.
107
+
108
+ .. versionadded:: 2.11
109
+ """
110
+ return value is False
111
+
112
+
113
+ def test_true(value: t.Any) -> bool:
114
+ """Return true if the object is True.
115
+
116
+ .. versionadded:: 2.11
117
+ """
118
+ return value is True
119
+
120
+
121
+ # NOTE: The existing 'number' test matches booleans and floats
122
+ def test_integer(value: t.Any) -> bool:
123
+ """Return true if the object is an integer.
124
+
125
+ .. versionadded:: 2.11
126
+ """
127
+ return isinstance(value, int) and value is not True and value is not False
128
+
129
+
130
+ # NOTE: The existing 'number' test matches booleans and integers
131
+ def test_float(value: t.Any) -> bool:
132
+ """Return true if the object is a float.
133
+
134
+ .. versionadded:: 2.11
135
+ """
136
+ return isinstance(value, float)
137
+
138
+
139
+ def test_lower(value: str) -> bool:
140
+ """Return true if the variable is lowercased."""
141
+ return str(value).islower()
142
+
143
+
144
+ def test_upper(value: str) -> bool:
145
+ """Return true if the variable is uppercased."""
146
+ return str(value).isupper()
147
+
148
+
149
+ def test_string(value: t.Any) -> bool:
150
+ """Return true if the object is a string."""
151
+ return isinstance(value, str)
152
+
153
+
154
+ def test_mapping(value: t.Any) -> bool:
155
+ """Return true if the object is a mapping (dict etc.).
156
+
157
+ .. versionadded:: 2.6
158
+ """
159
+ return isinstance(value, abc.Mapping)
160
+
161
+
162
+ def test_number(value: t.Any) -> bool:
163
+ """Return true if the variable is a number."""
164
+ return isinstance(value, Number)
165
+
166
+
167
+ def test_sequence(value: t.Any) -> bool:
168
+ """Return true if the variable is a sequence. Sequences are variables
169
+ that are iterable.
170
+ """
171
+ try:
172
+ len(value)
173
+ value.__getitem__ # noqa B018
174
+ except Exception:
175
+ return False
176
+
177
+ return True
178
+
179
+
180
+ def test_sameas(value: t.Any, other: t.Any) -> bool:
181
+ """Check if an object points to the same memory address than another
182
+ object:
183
+
184
+ .. sourcecode:: jinja
185
+
186
+ {% if foo.attribute is sameas false %}
187
+ the foo attribute really is the `False` singleton
188
+ {% endif %}
189
+ """
190
+ return value is other
191
+
192
+
193
+ def test_iterable(value: t.Any) -> bool:
194
+ """Check if it's possible to iterate over an object."""
195
+ try:
196
+ iter(value)
197
+ except TypeError:
198
+ return False
199
+
200
+ return True
201
+
202
+
203
+ def test_escaped(value: t.Any) -> bool:
204
+ """Check if the value is escaped."""
205
+ return hasattr(value, "__html__")
206
+
207
+
208
+ def test_in(value: t.Any, seq: t.Container[t.Any]) -> bool:
209
+ """Check if value is in seq.
210
+
211
+ .. versionadded:: 2.10
212
+ """
213
+ return value in seq
214
+
215
+
216
+ TESTS = {
217
+ "odd": test_odd,
218
+ "even": test_even,
219
+ "divisibleby": test_divisibleby,
220
+ "defined": test_defined,
221
+ "undefined": test_undefined,
222
+ "filter": test_filter,
223
+ "test": test_test,
224
+ "none": test_none,
225
+ "boolean": test_boolean,
226
+ "false": test_false,
227
+ "true": test_true,
228
+ "integer": test_integer,
229
+ "float": test_float,
230
+ "lower": test_lower,
231
+ "upper": test_upper,
232
+ "string": test_string,
233
+ "mapping": test_mapping,
234
+ "number": test_number,
235
+ "sequence": test_sequence,
236
+ "iterable": test_iterable,
237
+ "callable": callable,
238
+ "sameas": test_sameas,
239
+ "escaped": test_escaped,
240
+ "in": test_in,
241
+ "==": operator.eq,
242
+ "eq": operator.eq,
243
+ "equalto": operator.eq,
244
+ "!=": operator.ne,
245
+ "ne": operator.ne,
246
+ ">": operator.gt,
247
+ "gt": operator.gt,
248
+ "greaterthan": operator.gt,
249
+ "ge": operator.ge,
250
+ ">=": operator.ge,
251
+ "<": operator.lt,
252
+ "lt": operator.lt,
253
+ "lessthan": operator.lt,
254
+ "<=": operator.le,
255
+ "le": operator.le,
256
+ }