coco-framework 0.1.0__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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
"""Adapted from https://github.com/noahmorrison/chevron.
|
|
2
|
+
|
|
3
|
+
MIT License.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
14
|
+
Literal,
|
|
15
|
+
cast,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from typing import TypeAlias
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
Scopes: TypeAlias = list[Literal[False, 0] | Mapping[str, Any]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Globals
|
|
28
|
+
_CURRENT_LINE = 1
|
|
29
|
+
_LAST_TAG_LINE = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ChevronError(SyntaxError):
|
|
33
|
+
"""Custom exception for Chevron errors."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# Helper functions
|
|
38
|
+
#
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def grab_literal(template: str, l_del: str) -> tuple[str, str]:
|
|
42
|
+
"""Parse a literal from the template.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
template: The template to parse.
|
|
46
|
+
l_del: The left delimiter.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The literal and the template.
|
|
50
|
+
"""
|
|
51
|
+
global _CURRENT_LINE
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
# Look for the next tag and move the template to it
|
|
55
|
+
literal, template = template.split(l_del, 1)
|
|
56
|
+
_CURRENT_LINE += literal.count("\n")
|
|
57
|
+
|
|
58
|
+
# There are no more tags in the template?
|
|
59
|
+
except ValueError:
|
|
60
|
+
# Then the rest of the template is a literal
|
|
61
|
+
return (template, "")
|
|
62
|
+
|
|
63
|
+
return (literal, template)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def l_sa_check(
|
|
67
|
+
template: str, # noqa: ARG001
|
|
68
|
+
literal: str,
|
|
69
|
+
is_standalone: bool, # noqa: FBT001
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""Do a preliminary check to see if a tag could be a standalone.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
template: The template. (Not used.)
|
|
75
|
+
literal: The literal.
|
|
76
|
+
is_standalone: Whether the tag is standalone.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Whether the tag could be a standalone.
|
|
80
|
+
"""
|
|
81
|
+
# If there is a newline, or the previous tag was a standalone
|
|
82
|
+
if literal.find("\n") != -1 or is_standalone:
|
|
83
|
+
padding = literal.rsplit("\n", maxsplit=1)[-1]
|
|
84
|
+
|
|
85
|
+
# If all the characters since the last newline are spaces
|
|
86
|
+
# Then the next tag could be a standalone
|
|
87
|
+
# Otherwise it can't be
|
|
88
|
+
return padding.isspace() or not padding
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def r_sa_check(
|
|
93
|
+
template: str,
|
|
94
|
+
tag_type: str,
|
|
95
|
+
is_standalone: bool, # noqa: FBT001
|
|
96
|
+
) -> bool:
|
|
97
|
+
"""Do a final check to see if a tag could be a standalone.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
template: The template.
|
|
101
|
+
tag_type: The type of the tag.
|
|
102
|
+
is_standalone: Whether the tag is standalone.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Whether the tag could be a standalone.
|
|
106
|
+
"""
|
|
107
|
+
# Check right side if we might be a standalone
|
|
108
|
+
if is_standalone and tag_type not in {"variable", "no escape"}:
|
|
109
|
+
on_newline = template.split("\n", 1)
|
|
110
|
+
|
|
111
|
+
# If the stuff to the right of us are spaces we're a standalone
|
|
112
|
+
return on_newline[0].isspace() or not on_newline[0]
|
|
113
|
+
|
|
114
|
+
# If we're a tag can't be a standalone
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def parse_tag(template: str, l_del: str, r_del: str) -> tuple[tuple[str, str], str]:
|
|
119
|
+
"""Parse a tag from a template.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
template: The template.
|
|
123
|
+
l_del: The left delimiter.
|
|
124
|
+
r_del: The right delimiter.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The tag and the template.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ChevronError: If the tag is unclosed.
|
|
131
|
+
ChevronError: If the set delimiter tag is unclosed.
|
|
132
|
+
"""
|
|
133
|
+
tag_types = {
|
|
134
|
+
"!": "comment",
|
|
135
|
+
"#": "section",
|
|
136
|
+
"^": "inverted section",
|
|
137
|
+
"/": "end",
|
|
138
|
+
">": "partial",
|
|
139
|
+
"=": "set delimiter?",
|
|
140
|
+
"{": "no escape?",
|
|
141
|
+
"&": "no escape",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Get the tag
|
|
145
|
+
try:
|
|
146
|
+
tag, template = template.split(r_del, 1)
|
|
147
|
+
except ValueError as e:
|
|
148
|
+
msg = f"unclosed tag at line {_CURRENT_LINE}"
|
|
149
|
+
raise ChevronError(msg) from e
|
|
150
|
+
|
|
151
|
+
# Check for empty tags
|
|
152
|
+
if not tag.strip():
|
|
153
|
+
msg = f"empty tag at line {_CURRENT_LINE}"
|
|
154
|
+
raise ChevronError(msg)
|
|
155
|
+
|
|
156
|
+
# Find the type meaning of the first character
|
|
157
|
+
tag_type = tag_types.get(tag[0], "variable")
|
|
158
|
+
|
|
159
|
+
# If the type is not a variable
|
|
160
|
+
if tag_type != "variable":
|
|
161
|
+
# Then that first character is not needed
|
|
162
|
+
tag = tag[1:]
|
|
163
|
+
|
|
164
|
+
# If we might be a set delimiter tag
|
|
165
|
+
if tag_type == "set delimiter?":
|
|
166
|
+
# Double check to make sure we are
|
|
167
|
+
if tag.endswith("="):
|
|
168
|
+
tag_type = "set delimiter"
|
|
169
|
+
# Remove the equal sign
|
|
170
|
+
tag = tag[:-1]
|
|
171
|
+
|
|
172
|
+
# Otherwise we should complain
|
|
173
|
+
else:
|
|
174
|
+
msg = f"unclosed set delimiter tag\nat line {_CURRENT_LINE}"
|
|
175
|
+
raise ChevronError(msg)
|
|
176
|
+
|
|
177
|
+
elif (
|
|
178
|
+
# If we might be a no html escape tag
|
|
179
|
+
tag_type == "no escape?"
|
|
180
|
+
# And we have a third curly brace
|
|
181
|
+
# (And are using curly braces as delimiters)
|
|
182
|
+
and l_del == "{{"
|
|
183
|
+
and r_del == "}}"
|
|
184
|
+
and template.startswith("}")
|
|
185
|
+
):
|
|
186
|
+
# Then we are a no html escape tag
|
|
187
|
+
template = template[1:]
|
|
188
|
+
tag_type = "no escape"
|
|
189
|
+
|
|
190
|
+
# Strip the whitespace off the key and return
|
|
191
|
+
return ((tag_type, tag.strip()), template)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
#
|
|
195
|
+
# The main tokenizing function
|
|
196
|
+
#
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def tokenize(
|
|
200
|
+
template: str, def_ldel: str = "{{", def_rdel: str = "}}"
|
|
201
|
+
) -> Iterator[tuple[str, str]]:
|
|
202
|
+
"""Tokenize a mustache template.
|
|
203
|
+
|
|
204
|
+
Tokenizes a mustache template in a generator fashion, using file-like objects. It
|
|
205
|
+
also accepts a string containing the template.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
template: a file-like object, or a string of a mustache template
|
|
209
|
+
def_ldel: The default left delimiter
|
|
210
|
+
(`'{{'` by default, as in spec compliant mustache)
|
|
211
|
+
def_rdel: The default right delimiter
|
|
212
|
+
(`'}}'` by default, as in spec compliant mustache)
|
|
213
|
+
|
|
214
|
+
Yields:
|
|
215
|
+
Mustache tags in the form of a tuple `(tag_type, tag_key)` where `tag_type` is
|
|
216
|
+
one of:
|
|
217
|
+
|
|
218
|
+
* literal
|
|
219
|
+
* section
|
|
220
|
+
* inverted section
|
|
221
|
+
* end
|
|
222
|
+
* partial
|
|
223
|
+
* no escape
|
|
224
|
+
|
|
225
|
+
...and `tag_key` is either the key or in the case of a literal tag, the
|
|
226
|
+
literal itself.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ChevronError: If there is a syntax error in the template.
|
|
230
|
+
"""
|
|
231
|
+
global _CURRENT_LINE, _LAST_TAG_LINE
|
|
232
|
+
_CURRENT_LINE = 1
|
|
233
|
+
_LAST_TAG_LINE = None
|
|
234
|
+
|
|
235
|
+
is_standalone = True
|
|
236
|
+
open_sections = []
|
|
237
|
+
l_del = def_ldel
|
|
238
|
+
r_del = def_rdel
|
|
239
|
+
|
|
240
|
+
while template:
|
|
241
|
+
literal, template = grab_literal(template, l_del)
|
|
242
|
+
|
|
243
|
+
# If the template is completed
|
|
244
|
+
if not template:
|
|
245
|
+
# Then yield the literal and leave
|
|
246
|
+
yield ("literal", literal)
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
# Do the first check to see if we could be a standalone
|
|
250
|
+
is_standalone = l_sa_check(template, literal, is_standalone)
|
|
251
|
+
|
|
252
|
+
# Parse the tag
|
|
253
|
+
tag, template = parse_tag(template, l_del, r_del)
|
|
254
|
+
tag_type, tag_key = tag
|
|
255
|
+
|
|
256
|
+
# Special tag logic
|
|
257
|
+
|
|
258
|
+
# If we are a set delimiter tag
|
|
259
|
+
if tag_type == "set delimiter":
|
|
260
|
+
# Then get and set the delimiters
|
|
261
|
+
dels = tag_key.strip().split(" ")
|
|
262
|
+
l_del, r_del = dels[0], dels[-1]
|
|
263
|
+
|
|
264
|
+
# If we are a section tag
|
|
265
|
+
elif tag_type in {"section", "inverted section"}:
|
|
266
|
+
# Then open a new section
|
|
267
|
+
open_sections.append(tag_key)
|
|
268
|
+
_LAST_TAG_LINE = _CURRENT_LINE
|
|
269
|
+
|
|
270
|
+
# If we are an end tag
|
|
271
|
+
elif tag_type == "end":
|
|
272
|
+
# Then check to see if the last opened section
|
|
273
|
+
# is the same as us
|
|
274
|
+
try:
|
|
275
|
+
last_section = open_sections.pop()
|
|
276
|
+
except IndexError as e:
|
|
277
|
+
msg = (
|
|
278
|
+
f'Trying to close tag "{tag_key}"\n'
|
|
279
|
+
"Looks like it was not opened.\n"
|
|
280
|
+
f"line {_CURRENT_LINE + 1}"
|
|
281
|
+
)
|
|
282
|
+
raise ChevronError(msg) from e
|
|
283
|
+
if tag_key != last_section:
|
|
284
|
+
# Otherwise we need to complain
|
|
285
|
+
msg = (
|
|
286
|
+
f'Trying to close tag "{tag_key}"\n'
|
|
287
|
+
f'last open tag is "{last_section}"\n'
|
|
288
|
+
f"line {_CURRENT_LINE + 1}"
|
|
289
|
+
)
|
|
290
|
+
raise ChevronError(msg)
|
|
291
|
+
|
|
292
|
+
# Do the second check to see if we're a standalone
|
|
293
|
+
is_standalone = r_sa_check(template, tag_type, is_standalone)
|
|
294
|
+
|
|
295
|
+
# Which if we are
|
|
296
|
+
if is_standalone:
|
|
297
|
+
# Remove the stuff before the newline
|
|
298
|
+
template = template.split("\n", 1)[-1]
|
|
299
|
+
|
|
300
|
+
# Partials need to keep the spaces on their left
|
|
301
|
+
if tag_type != "partial":
|
|
302
|
+
# But other tags don't
|
|
303
|
+
literal = literal.rstrip(" ")
|
|
304
|
+
|
|
305
|
+
# Start yielding
|
|
306
|
+
# Ignore literals that are empty
|
|
307
|
+
if literal:
|
|
308
|
+
yield ("literal", literal)
|
|
309
|
+
|
|
310
|
+
# Ignore comments and set delimiters
|
|
311
|
+
if tag_type not in {"comment", "set delimiter?"}:
|
|
312
|
+
yield (tag_type, tag_key)
|
|
313
|
+
|
|
314
|
+
# If there are any open sections when we're done
|
|
315
|
+
if open_sections:
|
|
316
|
+
# Then we need to complain
|
|
317
|
+
msg = (
|
|
318
|
+
"Unexpected EOF\n"
|
|
319
|
+
f'the tag "{open_sections[-1]}" was never closed\n'
|
|
320
|
+
f"was opened at line {_LAST_TAG_LINE}"
|
|
321
|
+
)
|
|
322
|
+
raise ChevronError(msg)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
#
|
|
326
|
+
# Helper functions
|
|
327
|
+
#
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _html_escape(string: str) -> str:
|
|
331
|
+
"""Return the HTML-escaped string with these characters escaped: `" & < >`."""
|
|
332
|
+
html_codes = {
|
|
333
|
+
'"': """,
|
|
334
|
+
"<": "<",
|
|
335
|
+
">": ">",
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# & must be handled first
|
|
339
|
+
string = string.replace("&", "&")
|
|
340
|
+
for char, code in html_codes.items():
|
|
341
|
+
string = string.replace(char, code)
|
|
342
|
+
return string
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _get_key(
|
|
346
|
+
key: str,
|
|
347
|
+
scopes: Scopes,
|
|
348
|
+
*,
|
|
349
|
+
warn: bool,
|
|
350
|
+
keep: bool,
|
|
351
|
+
def_ldel: str,
|
|
352
|
+
def_rdel: str,
|
|
353
|
+
) -> Any:
|
|
354
|
+
"""Retrieve a value from the current scope using a dot-separated key path.
|
|
355
|
+
|
|
356
|
+
Traverses through nested dictionaries and lists using dot notation.
|
|
357
|
+
|
|
358
|
+
Supports special key `'.'` to return the current scope.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
key: Dot-separated key path (e.g., `'user.name'` or `'.'` for current scope).
|
|
362
|
+
scopes: List of scope dictionaries to search through.
|
|
363
|
+
warn: Whether to log a warning when a key is not found.
|
|
364
|
+
keep: Whether to return the original template tag when key is not found.
|
|
365
|
+
def_ldel: Left delimiter for template (used when keep is `True`).
|
|
366
|
+
def_rdel: Right delimiter for template (used when keep is `True`).
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
The value found at the key path.
|
|
370
|
+
|
|
371
|
+
If not found, returns the original template tag when keep is `True`,
|
|
372
|
+
otherwise returns an empty string.
|
|
373
|
+
"""
|
|
374
|
+
# If the key is a dot
|
|
375
|
+
if key == ".":
|
|
376
|
+
# Then just return the current scope
|
|
377
|
+
return scopes[0]
|
|
378
|
+
|
|
379
|
+
# Loop through the scopes
|
|
380
|
+
for scope in scopes:
|
|
381
|
+
try:
|
|
382
|
+
# Return an empty string if falsy, with two exceptions
|
|
383
|
+
# 0 should return 0, and False should return False
|
|
384
|
+
if scope in (0, False):
|
|
385
|
+
return scope
|
|
386
|
+
|
|
387
|
+
resolved_scope: Literal[False, 0] | Mapping[str, Any] | Sequence[Any] = (
|
|
388
|
+
scope
|
|
389
|
+
)
|
|
390
|
+
# For every dot separated key
|
|
391
|
+
for child in key.split("."):
|
|
392
|
+
# Return an empty string if falsy, with two exceptions
|
|
393
|
+
# 0 should return 0, and False should return False
|
|
394
|
+
if resolved_scope in (0, False):
|
|
395
|
+
return resolved_scope
|
|
396
|
+
# Move into the scope
|
|
397
|
+
if isinstance(resolved_scope, Mapping):
|
|
398
|
+
try:
|
|
399
|
+
resolved_scope = resolved_scope[child]
|
|
400
|
+
except (KeyError, TypeError):
|
|
401
|
+
# Key not found - will be caught by outer try-except
|
|
402
|
+
msg = f"Key {child!r} not found in dict"
|
|
403
|
+
raise KeyError(msg) from None
|
|
404
|
+
elif isinstance(resolved_scope, (list, tuple)):
|
|
405
|
+
try:
|
|
406
|
+
resolved_scope = resolved_scope[int(child)]
|
|
407
|
+
except (ValueError, IndexError, TypeError):
|
|
408
|
+
# Invalid index - will be caught by outer try-except
|
|
409
|
+
msg = f"Invalid index {child!r} for list/tuple"
|
|
410
|
+
raise IndexError(msg) from None
|
|
411
|
+
else:
|
|
412
|
+
# Reject everything else for security
|
|
413
|
+
# This prevents traversing into arbitrary Python objects
|
|
414
|
+
msg = (
|
|
415
|
+
f"Cannot traverse into {type(resolved_scope).__name__}. "
|
|
416
|
+
"Mustache templates only support dict, list, and tuple. "
|
|
417
|
+
f"Got: {type(resolved_scope)}"
|
|
418
|
+
)
|
|
419
|
+
raise TypeError(msg) # noqa: TRY301
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
# This allows for custom falsy data types
|
|
423
|
+
# https://github.com/noahmorrison/chevron/issues/35
|
|
424
|
+
if resolved_scope._CHEVRON_return_scope_when_falsy: # type: ignore[union-attr] # noqa: SLF001
|
|
425
|
+
return resolved_scope
|
|
426
|
+
except AttributeError:
|
|
427
|
+
if resolved_scope in (0, False):
|
|
428
|
+
return resolved_scope
|
|
429
|
+
return resolved_scope or ""
|
|
430
|
+
except (AttributeError, KeyError, IndexError, ValueError, TypeError):
|
|
431
|
+
# We couldn't find the key in the current scope
|
|
432
|
+
# TypeError: Attempted to traverse into non-dict/list type
|
|
433
|
+
# We'll try again on the next pass
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
# We couldn't find the key in any of the scopes
|
|
437
|
+
|
|
438
|
+
if warn:
|
|
439
|
+
logger.warning("Could not find key '%s'", key)
|
|
440
|
+
|
|
441
|
+
if keep:
|
|
442
|
+
return f"{def_ldel} {key} {def_rdel}"
|
|
443
|
+
|
|
444
|
+
return ""
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _get_partial(name: str, partials_dict: Mapping[str, str]) -> str:
|
|
448
|
+
"""Load a partial.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
The partial.
|
|
452
|
+
"""
|
|
453
|
+
try:
|
|
454
|
+
# Maybe the partial is in the dictionary
|
|
455
|
+
return partials_dict[name]
|
|
456
|
+
except KeyError:
|
|
457
|
+
return ""
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
#
|
|
461
|
+
# The main rendering function
|
|
462
|
+
#
|
|
463
|
+
g_token_cache: dict[str, list[tuple[str, str]]] = {}
|
|
464
|
+
|
|
465
|
+
EMPTY_DICT: MappingProxyType[str, str] = MappingProxyType({})
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def render(
|
|
469
|
+
template: str | list[tuple[str, str]] = "",
|
|
470
|
+
data: Mapping[str, Any] = EMPTY_DICT,
|
|
471
|
+
partials_dict: Mapping[str, str] = EMPTY_DICT,
|
|
472
|
+
padding: str = "",
|
|
473
|
+
def_ldel: str = "{{",
|
|
474
|
+
def_rdel: str = "}}",
|
|
475
|
+
scopes: Scopes | None = None,
|
|
476
|
+
warn: bool = False, # noqa: FBT001,FBT002
|
|
477
|
+
keep: bool = False, # noqa: FBT001,FBT002
|
|
478
|
+
) -> str:
|
|
479
|
+
"""Render a mustache template.
|
|
480
|
+
|
|
481
|
+
Renders a mustache template with a data scope and inline partial capability.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
template: A file-like object or a string containing the template.
|
|
485
|
+
data: A python dictionary with your data scope.
|
|
486
|
+
partials_dict: A python dictionary which will be search for partials
|
|
487
|
+
before the filesystem is.
|
|
488
|
+
|
|
489
|
+
`{'include': 'foo'}` is the same as a file called include.mustache
|
|
490
|
+
(defaults to `{}`).
|
|
491
|
+
padding: This is for padding partials, and shouldn't be used
|
|
492
|
+
(but can be if you really want to).
|
|
493
|
+
def_ldel: The default left delimiter
|
|
494
|
+
|
|
495
|
+
(`'{{'` by default, as in spec compliant mustache).
|
|
496
|
+
def_rdel: The default right delimiter
|
|
497
|
+
|
|
498
|
+
(`'}}'` by default, as in spec compliant mustache).
|
|
499
|
+
scopes: The list of scopes that `get_key` will look through.
|
|
500
|
+
warn: Log a warning when a template substitution isn't found in the data
|
|
501
|
+
keep: Keep unreplaced tags when a substitution isn't found in the data.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
A string containing the rendered template.
|
|
505
|
+
"""
|
|
506
|
+
# If the template is a sequence but not derived from a string
|
|
507
|
+
if isinstance(template, Sequence) and not isinstance(template, str):
|
|
508
|
+
# Then we don't need to tokenize it
|
|
509
|
+
# But it does need to be a generator
|
|
510
|
+
tokens: Iterator[tuple[str, str]] = (token for token in template)
|
|
511
|
+
elif template in g_token_cache:
|
|
512
|
+
tokens = (token for token in g_token_cache[template])
|
|
513
|
+
else:
|
|
514
|
+
# Otherwise make a generator
|
|
515
|
+
tokens = tokenize(template, def_ldel, def_rdel)
|
|
516
|
+
|
|
517
|
+
output = ""
|
|
518
|
+
|
|
519
|
+
if scopes is None:
|
|
520
|
+
scopes = [data]
|
|
521
|
+
|
|
522
|
+
# Run through the tokens
|
|
523
|
+
for tag, key in tokens:
|
|
524
|
+
# Set the current scope
|
|
525
|
+
current_scope = scopes[0]
|
|
526
|
+
|
|
527
|
+
# If we're an end tag
|
|
528
|
+
if tag == "end":
|
|
529
|
+
# Pop out of the latest scope
|
|
530
|
+
del scopes[0]
|
|
531
|
+
|
|
532
|
+
# If the current scope is falsy and not the only scope
|
|
533
|
+
elif not current_scope and len(scopes) != 1:
|
|
534
|
+
if tag in {"section", "inverted section"}:
|
|
535
|
+
# Set the most recent scope to a falsy value
|
|
536
|
+
scopes.insert(0, False)
|
|
537
|
+
|
|
538
|
+
# If we're a literal tag
|
|
539
|
+
elif tag == "literal":
|
|
540
|
+
# Add padding to the key and add it to the output
|
|
541
|
+
output += key.replace("\n", "\n" + padding)
|
|
542
|
+
|
|
543
|
+
# If we're a variable tag
|
|
544
|
+
elif tag == "variable":
|
|
545
|
+
# Add the html escaped key to the output
|
|
546
|
+
thing = _get_key(
|
|
547
|
+
key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel
|
|
548
|
+
)
|
|
549
|
+
if thing is True and key == ".":
|
|
550
|
+
# if we've coerced into a boolean by accident
|
|
551
|
+
# (inverted tags do this)
|
|
552
|
+
# then get the un-coerced object (next in the stack)
|
|
553
|
+
thing = scopes[1]
|
|
554
|
+
if not isinstance(thing, str):
|
|
555
|
+
thing = str(thing)
|
|
556
|
+
output += _html_escape(thing)
|
|
557
|
+
|
|
558
|
+
# If we're a no html escape tag
|
|
559
|
+
elif tag == "no escape":
|
|
560
|
+
# Just lookup the key and add it
|
|
561
|
+
thing = _get_key(
|
|
562
|
+
key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel
|
|
563
|
+
)
|
|
564
|
+
if not isinstance(thing, str):
|
|
565
|
+
thing = str(thing)
|
|
566
|
+
output += thing
|
|
567
|
+
|
|
568
|
+
# If we're a section tag
|
|
569
|
+
elif tag == "section":
|
|
570
|
+
# Get the sections scope
|
|
571
|
+
scope = _get_key(
|
|
572
|
+
key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# If the scope is a callable (as described in
|
|
576
|
+
# https://mustache.github.io/mustache.5.html)
|
|
577
|
+
if callable(scope):
|
|
578
|
+
# Generate template text from tags
|
|
579
|
+
text = ""
|
|
580
|
+
tags: list[tuple[str, str]] = []
|
|
581
|
+
for token in tokens:
|
|
582
|
+
if token == ("end", key):
|
|
583
|
+
break
|
|
584
|
+
|
|
585
|
+
tags.append(token)
|
|
586
|
+
tag_type, tag_key = token
|
|
587
|
+
if tag_type == "literal":
|
|
588
|
+
text += tag_key
|
|
589
|
+
elif tag_type == "no escape":
|
|
590
|
+
text += f"{def_ldel}& {tag_key} {def_rdel}"
|
|
591
|
+
else:
|
|
592
|
+
text += "{}{} {}{}".format(
|
|
593
|
+
def_ldel,
|
|
594
|
+
{
|
|
595
|
+
"comment": "!",
|
|
596
|
+
"section": "#",
|
|
597
|
+
"inverted section": "^",
|
|
598
|
+
"end": "/",
|
|
599
|
+
"partial": ">",
|
|
600
|
+
"set delimiter": "=",
|
|
601
|
+
"no escape": "&",
|
|
602
|
+
"variable": "",
|
|
603
|
+
}[tag_type],
|
|
604
|
+
tag_key,
|
|
605
|
+
def_rdel,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
g_token_cache[text] = tags
|
|
609
|
+
|
|
610
|
+
rend = scope(
|
|
611
|
+
text,
|
|
612
|
+
lambda template, data=None: render(
|
|
613
|
+
template,
|
|
614
|
+
data={},
|
|
615
|
+
partials_dict=partials_dict,
|
|
616
|
+
padding=padding,
|
|
617
|
+
def_ldel=def_ldel,
|
|
618
|
+
def_rdel=def_rdel,
|
|
619
|
+
scopes=(data and [data, *scopes]) or scopes,
|
|
620
|
+
warn=warn,
|
|
621
|
+
keep=keep,
|
|
622
|
+
),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
output += rend
|
|
626
|
+
|
|
627
|
+
# If the scope is a sequence, an iterator or generator but not
|
|
628
|
+
# derived from a string
|
|
629
|
+
elif isinstance(scope, (Sequence, Iterator)) and not isinstance(scope, str):
|
|
630
|
+
# Then we need to do some looping
|
|
631
|
+
|
|
632
|
+
# Gather up all the tags inside the section
|
|
633
|
+
# (And don't be tricked by nested end tags with the same key)
|
|
634
|
+
# TODO: This feels like it still has edge cases, no?
|
|
635
|
+
tags = []
|
|
636
|
+
tags_with_same_key = 0
|
|
637
|
+
for token in tokens:
|
|
638
|
+
if token == ("section", key):
|
|
639
|
+
tags_with_same_key += 1
|
|
640
|
+
if token == ("end", key):
|
|
641
|
+
tags_with_same_key -= 1
|
|
642
|
+
if tags_with_same_key < 0:
|
|
643
|
+
break
|
|
644
|
+
tags.append(token)
|
|
645
|
+
|
|
646
|
+
# For every item in the scope
|
|
647
|
+
for thing in scope:
|
|
648
|
+
# Append it as the most recent scope and render
|
|
649
|
+
new_scope = [thing, *scopes]
|
|
650
|
+
rend = render(
|
|
651
|
+
template=tags,
|
|
652
|
+
scopes=new_scope,
|
|
653
|
+
padding=padding,
|
|
654
|
+
partials_dict=partials_dict,
|
|
655
|
+
def_ldel=def_ldel,
|
|
656
|
+
def_rdel=def_rdel,
|
|
657
|
+
warn=warn,
|
|
658
|
+
keep=keep,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
output += rend
|
|
662
|
+
|
|
663
|
+
else:
|
|
664
|
+
# Otherwise we're just a scope section
|
|
665
|
+
scopes.insert(0, scope)
|
|
666
|
+
|
|
667
|
+
# If we're an inverted section
|
|
668
|
+
elif tag == "inverted section":
|
|
669
|
+
# Add the flipped scope to the scopes
|
|
670
|
+
scope = _get_key(
|
|
671
|
+
key, scopes, warn=warn, keep=keep, def_ldel=def_ldel, def_rdel=def_rdel
|
|
672
|
+
)
|
|
673
|
+
scopes.insert(0, cast("Literal[False]", not scope))
|
|
674
|
+
|
|
675
|
+
# If we're a partial
|
|
676
|
+
elif tag == "partial":
|
|
677
|
+
# Load the partial
|
|
678
|
+
partial = _get_partial(key, partials_dict)
|
|
679
|
+
|
|
680
|
+
# Find what to pad the partial with
|
|
681
|
+
left = output.rpartition("\n")[2]
|
|
682
|
+
part_padding = padding
|
|
683
|
+
if left.isspace():
|
|
684
|
+
part_padding += left
|
|
685
|
+
|
|
686
|
+
# Render the partial
|
|
687
|
+
part_out = render(
|
|
688
|
+
template=partial,
|
|
689
|
+
partials_dict=partials_dict,
|
|
690
|
+
def_ldel=def_ldel,
|
|
691
|
+
def_rdel=def_rdel,
|
|
692
|
+
padding=part_padding,
|
|
693
|
+
scopes=scopes,
|
|
694
|
+
warn=warn,
|
|
695
|
+
keep=keep,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# If the partial was indented
|
|
699
|
+
if left.isspace():
|
|
700
|
+
# then remove the spaces from the end
|
|
701
|
+
part_out = part_out.rstrip(" \t")
|
|
702
|
+
|
|
703
|
+
# Add the partials output to the output
|
|
704
|
+
output += part_out
|
|
705
|
+
|
|
706
|
+
return output
|