quantum-framework 0.18.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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum expression evaluator.
|
|
3
|
+
|
|
4
|
+
One evaluator, built on `ast.parse` with a node whitelist, to replace the two
|
|
5
|
+
parallel hand-rolled ones (6 `_evaluate_*` methods in runtime/component.py, 4
|
|
6
|
+
more in runtime/renderer.py) and the six `eval()` calls they rely on. See
|
|
7
|
+
FRAMEWORK_PLAN.md Fase 2.1.
|
|
8
|
+
|
|
9
|
+
Two things this fixes by construction:
|
|
10
|
+
|
|
11
|
+
- **Security.** `eval()` with an emptied `__builtins__` is not a sandbox — it
|
|
12
|
+
is bypassable through attribute traversal
|
|
13
|
+
(`().__class__.__mro__[1].__subclasses__()`). A whitelist of AST node types
|
|
14
|
+
cannot be talked out of its own grammar. This closes P0.1 of
|
|
15
|
+
PUBLIC_RELEASE_PLAN.md.
|
|
16
|
+
- **Coercion.** `{a + b}` where the values arrived as strings — from an LLM
|
|
17
|
+
tool call, a form field, a query parameter — concatenated instead of adding.
|
|
18
|
+
Numeric-looking operands are coerced for arithmetic and comparison.
|
|
19
|
+
|
|
20
|
+
The grammar is deliberately small, and it was measured rather than guessed:
|
|
21
|
+
across 1,224 expressions in examples/*.q, 57% are a bare variable or a dotted
|
|
22
|
+
access, and the whole arithmetic/comparison/call surface is about 15%.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
import contextvars
|
|
27
|
+
import math
|
|
28
|
+
import operator
|
|
29
|
+
import re
|
|
30
|
+
from typing import Any, Callable, Dict, Optional
|
|
31
|
+
|
|
32
|
+
from quantum.core.expression_stdlib import STDLIB
|
|
33
|
+
|
|
34
|
+
# EXPR-8: in a condition, a key a scope does not have (`session.userId`
|
|
35
|
+
# before login) is absent, not a failure — see ExpressionEvaluator.evaluate.
|
|
36
|
+
_SCOPES = ('session', 'application', 'request', 'cookie', 'form', 'query')
|
|
37
|
+
_ABSENT_SCOPE_KEY_IS_NONE = contextvars.ContextVar('absent_scope_key_is_none', default=False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExpressionError(Exception):
|
|
41
|
+
"""Raised when an expression cannot be evaluated."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UndefinedError(ExpressionError):
|
|
45
|
+
"""A name, key or attribute that does not exist.
|
|
46
|
+
|
|
47
|
+
Kept apart from the other failures because a condition is a presence test
|
|
48
|
+
(EXPR-5): `<q:if condition="flash">` is false before there is a flash, but
|
|
49
|
+
`a === b` or a misspelled function is still an error.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# `{n}`, `{n,}` and `{n,m}` are regex quantifiers, not Quantum expressions.
|
|
54
|
+
# They occur for real: examples/form_validation.q has pattern="\d{10,11}" and
|
|
55
|
+
# examples/python-scripting.q has r'[a-zA-Z]{2,}'. The old evaluator left them
|
|
56
|
+
# alone only by accident — it failed to evaluate them and handed the literal
|
|
57
|
+
# back. This evaluator would happily read `{10,11}` as the tuple (10, 11) and
|
|
58
|
+
# silently corrupt the validation pattern, so the case is excluded on purpose.
|
|
59
|
+
#
|
|
60
|
+
# The underlying gap is that Quantum has no escape for a literal brace. Until
|
|
61
|
+
# it has one, this heuristic is the seam, and it is deliberately narrow:
|
|
62
|
+
# digits and at most one comma, nothing else.
|
|
63
|
+
_REGEX_QUANTIFIER = re.compile(r'^\d+\s*(,\s*\d*)?$')
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_regex_quantifier(content: str) -> bool:
|
|
67
|
+
"""True when a `{...}` group should be left as literal text.
|
|
68
|
+
|
|
69
|
+
A q: attribute that is ONLY `{5}` is the number 5 (EXPR-7) — the runtime
|
|
70
|
+
checks that case before calling this. Inside other text, `[0-9]{3}` stays a
|
|
71
|
+
quantifier.
|
|
72
|
+
"""
|
|
73
|
+
return bool(_REGEX_QUANTIFIER.match(content.strip()))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Node types the evaluator accepts. Anything else is refused by construction —
|
|
77
|
+
# notably Lambda, comprehensions, walrus, imports, and any form of statement.
|
|
78
|
+
_ALLOWED_NODES = (
|
|
79
|
+
ast.Expression, ast.Constant, ast.Name, ast.Load,
|
|
80
|
+
ast.BinOp, ast.UnaryOp, ast.BoolOp, ast.Compare, ast.IfExp,
|
|
81
|
+
ast.Attribute, ast.Subscript, ast.Slice,
|
|
82
|
+
ast.Dict, ast.List, ast.Tuple, ast.Set,
|
|
83
|
+
ast.Call, ast.keyword,
|
|
84
|
+
# operators
|
|
85
|
+
ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow,
|
|
86
|
+
ast.USub, ast.UAdd, ast.Not,
|
|
87
|
+
ast.And, ast.Or,
|
|
88
|
+
ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
_BIN_OPS: Dict[type, Callable[[Any, Any], Any]] = {
|
|
92
|
+
ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
|
|
93
|
+
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
|
|
94
|
+
ast.Mod: operator.mod, ast.Pow: operator.pow,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_CMP_OPS: Dict[type, Callable[[Any, Any], Any]] = {
|
|
98
|
+
ast.Eq: operator.eq, ast.NotEq: operator.ne,
|
|
99
|
+
ast.Lt: operator.lt, ast.LtE: operator.le,
|
|
100
|
+
ast.Gt: operator.gt, ast.GtE: operator.ge,
|
|
101
|
+
ast.In: lambda a, b: a in b, ast.NotIn: lambda a, b: a not in b,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_UNARY_OPS: Dict[type, Callable[[Any], Any]] = {
|
|
105
|
+
ast.USub: operator.neg, ast.UAdd: operator.pos, ast.Not: operator.not_,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def coerce_number(value: Any) -> Any:
|
|
110
|
+
"""Turn a numeric-looking string into a number, leave everything else.
|
|
111
|
+
|
|
112
|
+
This is the difference between {a + b} adding and concatenating when the
|
|
113
|
+
values came from an LLM, a form post or a query parameter — all of which
|
|
114
|
+
deliver strings.
|
|
115
|
+
"""
|
|
116
|
+
if isinstance(value, bool) or not isinstance(value, str):
|
|
117
|
+
return value
|
|
118
|
+
text = value.strip()
|
|
119
|
+
if not text:
|
|
120
|
+
return value
|
|
121
|
+
try:
|
|
122
|
+
return int(text)
|
|
123
|
+
except ValueError:
|
|
124
|
+
pass
|
|
125
|
+
try:
|
|
126
|
+
return float(text)
|
|
127
|
+
except ValueError:
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _names_of(target: Any) -> list:
|
|
132
|
+
"""The names a reader could plausibly have meant, for a suggestion."""
|
|
133
|
+
if isinstance(target, dict):
|
|
134
|
+
return [k for k in target if isinstance(k, str) and not k.startswith('_')]
|
|
135
|
+
return [a for a in dir(target) if not a.startswith('_')]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _suggest(wanted: str, target: Any, limit: int = 3) -> str:
|
|
139
|
+
"""`, did you mean x, y?` — or nothing when there is no near match.
|
|
140
|
+
|
|
141
|
+
The reason this exists: writing a real screen against Quantum, the two
|
|
142
|
+
failures that cost the most time were {p.name} (var= had been silently
|
|
143
|
+
discarded, so `p` did not exist) and {projects.recordCount} (the metadata
|
|
144
|
+
lives on `projects_result`). Both were resolvable in seconds from a list of
|
|
145
|
+
what WAS in scope, and neither message offered one. See DOGFOOD_NOTES.md.
|
|
146
|
+
"""
|
|
147
|
+
import difflib
|
|
148
|
+
names = _names_of(target)
|
|
149
|
+
if not names:
|
|
150
|
+
return ""
|
|
151
|
+
close = difflib.get_close_matches(wanted, names, n=limit, cutoff=0.6)
|
|
152
|
+
if close:
|
|
153
|
+
return ", did you mean " + " or ".join(repr(c) for c in close) + "?"
|
|
154
|
+
# No near match: naming what IS available beats naming what is not, but
|
|
155
|
+
# only while the list is short enough to read.
|
|
156
|
+
if len(names) <= 8:
|
|
157
|
+
return " (in scope: " + ", ".join(sorted(names)) + ")"
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# A template is evaluated inside a request. An expression that never returns
|
|
162
|
+
# is a hung worker, and these two do it with three characters:
|
|
163
|
+
#
|
|
164
|
+
# {9**9**9} -> does not return
|
|
165
|
+
# {10**100000000} -> does not return
|
|
166
|
+
# ({'a' * 999999999} allocated ~1 GB; since EXPR-7 `*` needs numbers.)
|
|
167
|
+
#
|
|
168
|
+
# No dunder, no escape, no attacker — a typo in a template is enough, and there
|
|
169
|
+
# is no timeout anywhere above this to catch it. The caps are far above any
|
|
170
|
+
# real expression: 4096 bits is a 1233-digit number.
|
|
171
|
+
_MAX_POW_BITS = 4096
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _guard_pow(left: Any, right: Any) -> None:
|
|
175
|
+
"""Recusa `a ** b` grande demais — inteiro OU float.
|
|
176
|
+
|
|
177
|
+
Exigia `isinstance` int nos DOIS lados, entao qualquer mistura passava
|
|
178
|
+
direto: `2.0 ** 100000000` e `2 ** 1e9` chegavam ao operador e
|
|
179
|
+
levantavam `OverflowError`, que nao e `ExpressionError` — subia pelo
|
|
180
|
+
handler da requisicao como um 500 sem explicacao, em vez da mensagem que
|
|
181
|
+
esta funcao existe para dar.
|
|
182
|
+
"""
|
|
183
|
+
if isinstance(left, bool) or isinstance(right, bool):
|
|
184
|
+
return
|
|
185
|
+
if not isinstance(left, (int, float)) or not isinstance(right, (int, float)):
|
|
186
|
+
return
|
|
187
|
+
if right < 0 or left in (0, 1, -1):
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
if isinstance(left, int) and isinstance(right, int):
|
|
191
|
+
# bits(left**right) ≈ right * bits(left)
|
|
192
|
+
bits = right * max(left.bit_length(), 1)
|
|
193
|
+
else:
|
|
194
|
+
base = abs(float(left))
|
|
195
|
+
if base <= 1.0:
|
|
196
|
+
return
|
|
197
|
+
bits = float(right) * math.log2(base)
|
|
198
|
+
except (OverflowError, ValueError):
|
|
199
|
+
# O proprio calculo do tamanho estourou: e grande demais.
|
|
200
|
+
bits = float('inf')
|
|
201
|
+
|
|
202
|
+
if bits > _MAX_POW_BITS:
|
|
203
|
+
raise ExpressionError(
|
|
204
|
+
f"{left}**{right} is too large to compute "
|
|
205
|
+
f"(over {_MAX_POW_BITS} bits); this would hang the request"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _both_numeric(left: Any, right: Any) -> bool:
|
|
210
|
+
return isinstance(left, (int, float)) and isinstance(right, (int, float)) \
|
|
211
|
+
and not isinstance(left, bool) and not isinstance(right, bool)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _normalise_js_operators(source: str) -> str:
|
|
216
|
+
"""Rewrite JavaScript-style `!x`, `a && b` and `a || b` into Python's
|
|
217
|
+
`not`, `and` and `or`.
|
|
218
|
+
|
|
219
|
+
Real .q files use them ({!result.success} appears across the examples, and
|
|
220
|
+
docs/guide/conditionals.md teaches `age >= 18 && hasLicense`), so they are
|
|
221
|
+
part of the language surface. `&&` and `||` were never translated: the
|
|
222
|
+
condition failed to parse and, read as false, took the else branch for
|
|
223
|
+
every input. `!=` is left alone, and nothing inside a string literal is
|
|
224
|
+
touched.
|
|
225
|
+
"""
|
|
226
|
+
if '!' not in source and '&&' not in source and '||' not in source:
|
|
227
|
+
return source
|
|
228
|
+
|
|
229
|
+
out = []
|
|
230
|
+
i = 0
|
|
231
|
+
quote = None
|
|
232
|
+
while i < len(source):
|
|
233
|
+
ch = source[i]
|
|
234
|
+
if quote:
|
|
235
|
+
out.append(ch)
|
|
236
|
+
if ch == quote and (i == 0 or source[i - 1] != "\\"):
|
|
237
|
+
quote = None
|
|
238
|
+
i += 1
|
|
239
|
+
continue
|
|
240
|
+
if ch in ('"', "'"):
|
|
241
|
+
quote = ch
|
|
242
|
+
out.append(ch)
|
|
243
|
+
i += 1
|
|
244
|
+
continue
|
|
245
|
+
if ch == '!' and i + 1 < len(source) and source[i + 1] != '=':
|
|
246
|
+
out.append('not ')
|
|
247
|
+
i += 1
|
|
248
|
+
continue
|
|
249
|
+
if source[i:i + 2] in ('&&', '||'):
|
|
250
|
+
out.append(' and ' if ch == '&' else ' or ')
|
|
251
|
+
i += 2
|
|
252
|
+
continue
|
|
253
|
+
out.append(ch)
|
|
254
|
+
i += 1
|
|
255
|
+
return ''.join(out)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# EXPR-10: what a JavaScript habit is called here. `Math.ceil(x)` and
|
|
259
|
+
# `text.split(' ')` are the first things people write; the bare "only named
|
|
260
|
+
# function calls are allowed" did not say what to write instead.
|
|
261
|
+
_JS_EQUIVALENTS = {
|
|
262
|
+
'Math.ceil': 'ceil(x)', 'Math.floor': 'floor(x)', 'Math.round': 'round(x)',
|
|
263
|
+
'Math.max': 'max(a, b)', 'Math.min': 'min(a, b)', 'Math.abs': 'abs(x)',
|
|
264
|
+
'Date.now': 'now()', 'parseInt': 'int(x)', 'parseFloat': 'float(x)',
|
|
265
|
+
'split': "split(text, ' ')", 'join': "join(items, ', ')", 'toUpperCase': 'upper(text)',
|
|
266
|
+
'toLowerCase': 'lower(text)', 'trim': 'trim(text)', 'replace': 'replace(text, old, new)',
|
|
267
|
+
'includes': 'contains(text, part)', 'indexOf': 'contains(text, part)',
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _method_hint(func: ast.AST) -> str:
|
|
272
|
+
if not isinstance(func, ast.Attribute):
|
|
273
|
+
return ""
|
|
274
|
+
base = func.value.id if isinstance(func.value, ast.Name) else None
|
|
275
|
+
chave = f"{base}.{func.attr}" if base in ('Math', 'Date') else func.attr
|
|
276
|
+
if chave in _JS_EQUIVALENTS:
|
|
277
|
+
escrito = chave if '.' in chave else f"x.{func.attr}"
|
|
278
|
+
return f"; write {_JS_EQUIVALENTS[chave]} instead of {escrito}(...)"
|
|
279
|
+
return f"; call functions by name, e.g. {func.attr}(x), not x.{func.attr}()"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def names_read(expression: str) -> set:
|
|
283
|
+
"""The root names an expression reads — `user` in `user.name > 1` — not the
|
|
284
|
+
functions it calls. An expression that does not parse reads nothing here;
|
|
285
|
+
evaluating it reports the error."""
|
|
286
|
+
try:
|
|
287
|
+
tree = ast.parse(_normalise_js_operators(expression.strip()), mode='eval')
|
|
288
|
+
except SyntaxError:
|
|
289
|
+
return set()
|
|
290
|
+
chamadas = {id(n.func) for n in ast.walk(tree) if isinstance(n, ast.Call)}
|
|
291
|
+
return {n.id for n in ast.walk(tree)
|
|
292
|
+
if isinstance(n, ast.Name) and id(n) not in chamadas}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class ExpressionEvaluator:
|
|
296
|
+
"""Evaluates Quantum `{...}` expressions against a variable context."""
|
|
297
|
+
|
|
298
|
+
def __init__(self, functions: Optional[Dict[str, Callable]] = None,
|
|
299
|
+
max_length: int = 2000,
|
|
300
|
+
function_resolver: Optional[Callable[[str], Optional[Callable]]] = None):
|
|
301
|
+
"""
|
|
302
|
+
Args:
|
|
303
|
+
functions: extra callables available to expressions.
|
|
304
|
+
max_length: refuse expressions longer than this.
|
|
305
|
+
function_resolver: consulted for a name that is not in
|
|
306
|
+
`functions`. This is how a component's own q:function
|
|
307
|
+
becomes callable from `{somaTotal(itens)}` — the registry
|
|
308
|
+
is filled while the component executes, so it cannot be
|
|
309
|
+
handed over as a dict when the evaluator is built.
|
|
310
|
+
"""
|
|
311
|
+
# Stdlib plus whatever the component registered (q:function).
|
|
312
|
+
self.functions: Dict[str, Callable] = dict(STDLIB)
|
|
313
|
+
if functions:
|
|
314
|
+
self.functions.update(functions)
|
|
315
|
+
self.max_length = max_length
|
|
316
|
+
self.function_resolver = function_resolver
|
|
317
|
+
|
|
318
|
+
# -- public API --------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
def evaluate(self, expression: str, context: Dict[str, Any],
|
|
321
|
+
absent_scope_key_is_none: bool = False) -> Any:
|
|
322
|
+
"""Evaluate an expression, raising ExpressionError on any failure.
|
|
323
|
+
|
|
324
|
+
absent_scope_key_is_none (conditions, EXPR-8): `session.x` where the
|
|
325
|
+
scope has no `x` reads as None instead of raising. A condition is a
|
|
326
|
+
presence test, and a scope key that is not there yet is the ordinary
|
|
327
|
+
case (a page runs before login) — raising made the WHOLE condition
|
|
328
|
+
false, so `not session.authenticated` was false for a visitor with no
|
|
329
|
+
session and a guard written that way never fired.
|
|
330
|
+
"""
|
|
331
|
+
marca = _ABSENT_SCOPE_KEY_IS_NONE.set(absent_scope_key_is_none)
|
|
332
|
+
try:
|
|
333
|
+
return self._evaluate(expression, context)
|
|
334
|
+
finally:
|
|
335
|
+
_ABSENT_SCOPE_KEY_IS_NONE.reset(marca)
|
|
336
|
+
|
|
337
|
+
def _evaluate(self, expression: str, context: Dict[str, Any]) -> Any:
|
|
338
|
+
if expression is None:
|
|
339
|
+
raise ExpressionError("expression is None")
|
|
340
|
+
|
|
341
|
+
source = expression.strip()
|
|
342
|
+
if not source:
|
|
343
|
+
raise ExpressionError("empty expression")
|
|
344
|
+
if len(source) > self.max_length:
|
|
345
|
+
raise ExpressionError(f"expression exceeds {self.max_length} characters")
|
|
346
|
+
|
|
347
|
+
source = _normalise_js_operators(source)
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
tree = ast.parse(source, mode='eval')
|
|
351
|
+
except SyntaxError as exc:
|
|
352
|
+
raise ExpressionError(f"invalid expression {source!r}: {exc.msg}") from exc
|
|
353
|
+
|
|
354
|
+
self._validate(tree, source)
|
|
355
|
+
try:
|
|
356
|
+
return self._eval(tree.body, context)
|
|
357
|
+
except RecursionError as exc:
|
|
358
|
+
# A guarda de profundidade cobre a EXPRESSAO; o contexto tambem
|
|
359
|
+
# pode ser fundo (um dict que referencia a si mesmo, por
|
|
360
|
+
# exemplo). Uma expressao de template nunca pode derrubar a
|
|
361
|
+
# requisicao com um erro que nao seja ExpressionError.
|
|
362
|
+
raise ExpressionError(
|
|
363
|
+
f"evaluating {source[:60]!r} ran out of stack") from exc
|
|
364
|
+
|
|
365
|
+
def try_evaluate(self, expression: str, context: Dict[str, Any],
|
|
366
|
+
default: Any = None) -> Any:
|
|
367
|
+
"""Evaluate, returning `default` instead of raising.
|
|
368
|
+
|
|
369
|
+
For render paths, where a bad expression should not take down the page.
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
return self.evaluate(expression, context)
|
|
373
|
+
except ExpressionError:
|
|
374
|
+
return default
|
|
375
|
+
|
|
376
|
+
# -- validation --------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
# Profundidade maxima da arvore. `_eval` desce recursivamente, e o
|
|
379
|
+
# limite de recursao do Python e ~1000 quadros COMPARTILHADOS com tudo
|
|
380
|
+
# que ja esta na pilha (Flask, o executor, o renderer). `{-------...1}`
|
|
381
|
+
# com mil sinais levantava RecursionError — que nao e ExpressionError e
|
|
382
|
+
# portanto virava 500 — e ainda deixava o interpretador raspando o
|
|
383
|
+
# limite. Cem niveis e mais fundo do que qualquer expressao de template
|
|
384
|
+
# real e fica bem longe da borda.
|
|
385
|
+
MAX_DEPTH = 100
|
|
386
|
+
|
|
387
|
+
def _validate(self, tree: ast.AST, source: str):
|
|
388
|
+
self._check_depth(tree.body if isinstance(tree, ast.Expression)
|
|
389
|
+
else tree, source)
|
|
390
|
+
for node in ast.walk(tree):
|
|
391
|
+
if not isinstance(node, _ALLOWED_NODES):
|
|
392
|
+
raise ExpressionError(
|
|
393
|
+
f"{type(node).__name__} is not allowed in expressions "
|
|
394
|
+
f"({source!r})"
|
|
395
|
+
)
|
|
396
|
+
# Dunder traversal is the classic eval sandbox escape.
|
|
397
|
+
if isinstance(node, ast.Attribute) and node.attr.startswith('__'):
|
|
398
|
+
raise ExpressionError(
|
|
399
|
+
f"attribute {node.attr!r} is not accessible ({source!r})"
|
|
400
|
+
)
|
|
401
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str) \
|
|
402
|
+
and node.value.startswith('__'):
|
|
403
|
+
raise ExpressionError(f"dunder string is not allowed ({source!r})")
|
|
404
|
+
|
|
405
|
+
def _check_depth(self, root: ast.AST, source: str) -> None:
|
|
406
|
+
"""Mede a profundidade sem recursao — medir recursivamente teria o
|
|
407
|
+
mesmo problema que a medicao existe para evitar."""
|
|
408
|
+
pilha = [(root, 1)]
|
|
409
|
+
while pilha:
|
|
410
|
+
node, nivel = pilha.pop()
|
|
411
|
+
if nivel > self.MAX_DEPTH:
|
|
412
|
+
raise ExpressionError(
|
|
413
|
+
f"expression is nested more than {self.MAX_DEPTH} levels "
|
|
414
|
+
f"deep ({source[:60]!r}...); this would exhaust the stack"
|
|
415
|
+
)
|
|
416
|
+
for filho in ast.iter_child_nodes(node):
|
|
417
|
+
pilha.append((filho, nivel + 1))
|
|
418
|
+
|
|
419
|
+
# -- evaluation --------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
def _eval(self, node: ast.AST, ctx: Dict[str, Any]) -> Any:
|
|
422
|
+
if isinstance(node, ast.Constant):
|
|
423
|
+
return node.value
|
|
424
|
+
|
|
425
|
+
if isinstance(node, ast.Name):
|
|
426
|
+
return self._lookup(node.id, ctx)
|
|
427
|
+
|
|
428
|
+
if isinstance(node, ast.BinOp):
|
|
429
|
+
left = self._eval(node.left, ctx)
|
|
430
|
+
right = self._eval(node.right, ctx)
|
|
431
|
+
op = _BIN_OPS.get(type(node.op))
|
|
432
|
+
if op is None:
|
|
433
|
+
raise ExpressionError(f"unsupported operator {type(node.op).__name__}")
|
|
434
|
+
|
|
435
|
+
# String + string stays concatenation only when neither side is a
|
|
436
|
+
# number in disguise.
|
|
437
|
+
cl, cr = coerce_number(left), coerce_number(right)
|
|
438
|
+
if _both_numeric(cl, cr):
|
|
439
|
+
left, right = cl, cr
|
|
440
|
+
elif isinstance(node.op, ast.Add) and not (
|
|
441
|
+
(isinstance(left, str) and isinstance(right, str))
|
|
442
|
+
or (isinstance(left, list) and isinstance(right, list))):
|
|
443
|
+
# EXPR-7: '+' adds numbers or joins two texts / two lists. The
|
|
444
|
+
# mixed case used to surface Python's "can only concatenate
|
|
445
|
+
# str (not "int") to str".
|
|
446
|
+
raise ExpressionError(
|
|
447
|
+
f"'+' needs two numbers, two texts or two lists, got {left!r} and {right!r}")
|
|
448
|
+
elif not isinstance(node.op, ast.Add):
|
|
449
|
+
# EXPR-7: arithmetic is on numbers. Python would repeat text
|
|
450
|
+
# ('ab' * 3), format it ('%s' % x) or fail with its own
|
|
451
|
+
# message; `n * factorial(n - 1)` whose base case returned the
|
|
452
|
+
# text '{1}' produced '{1}{1}{1}{1}{1}' and no error.
|
|
453
|
+
simbolo = {ast.Sub: '-', ast.Mult: '*', ast.Div: '/', ast.FloorDiv: '//',
|
|
454
|
+
ast.Mod: '%', ast.Pow: '**'}[type(node.op)]
|
|
455
|
+
raise ExpressionError(
|
|
456
|
+
f"'{simbolo}' needs two numbers, got {left!r} and {right!r}")
|
|
457
|
+
|
|
458
|
+
# Refuse what would not finish, before attempting it.
|
|
459
|
+
if isinstance(node.op, ast.Pow):
|
|
460
|
+
_guard_pow(left, right)
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
return op(left, right)
|
|
464
|
+
except ZeroDivisionError:
|
|
465
|
+
raise ExpressionError("division by zero")
|
|
466
|
+
except TypeError as exc:
|
|
467
|
+
raise ExpressionError(f"cannot apply operator: {exc}") from exc
|
|
468
|
+
except (OverflowError, MemoryError) as exc:
|
|
469
|
+
# `1e300 ** 2` cabe na estimativa de bits e estoura assim
|
|
470
|
+
# mesmo, porque float tem expoente maximo. Sem isto o erro
|
|
471
|
+
# nao e ExpressionError e sobe como 500.
|
|
472
|
+
raise ExpressionError(
|
|
473
|
+
f"the result is too large to represent: {exc}") from exc
|
|
474
|
+
|
|
475
|
+
if isinstance(node, ast.UnaryOp):
|
|
476
|
+
operand = self._eval(node.operand, ctx)
|
|
477
|
+
op = _UNARY_OPS.get(type(node.op))
|
|
478
|
+
if op is None:
|
|
479
|
+
raise ExpressionError(f"unsupported unary {type(node.op).__name__}")
|
|
480
|
+
if isinstance(node.op, (ast.USub, ast.UAdd)):
|
|
481
|
+
operand = coerce_number(operand)
|
|
482
|
+
return op(operand)
|
|
483
|
+
|
|
484
|
+
if isinstance(node, ast.BoolOp):
|
|
485
|
+
# EXPR-6: short-circuit, as in Python. Every operand used to be
|
|
486
|
+
# evaluated first, so `user and user.name` failed with "variable
|
|
487
|
+
# 'user' is not defined"-style errors exactly when the left side
|
|
488
|
+
# was there to guard the right.
|
|
489
|
+
valor = False
|
|
490
|
+
for operando in node.values:
|
|
491
|
+
valor = self._eval(operando, ctx)
|
|
492
|
+
if isinstance(node.op, ast.And) and not valor:
|
|
493
|
+
return valor
|
|
494
|
+
if isinstance(node.op, ast.Or) and valor:
|
|
495
|
+
return valor
|
|
496
|
+
return valor
|
|
497
|
+
|
|
498
|
+
if isinstance(node, ast.Compare):
|
|
499
|
+
left = self._eval(node.left, ctx)
|
|
500
|
+
for op_node, comparator in zip(node.ops, node.comparators):
|
|
501
|
+
right = self._eval(comparator, ctx)
|
|
502
|
+
op = _CMP_OPS.get(type(op_node))
|
|
503
|
+
if op is None:
|
|
504
|
+
raise ExpressionError(f"unsupported comparison {type(op_node).__name__}")
|
|
505
|
+
cl, cr = coerce_number(left), coerce_number(right)
|
|
506
|
+
if _both_numeric(cl, cr):
|
|
507
|
+
left_cmp, right_cmp = cl, cr
|
|
508
|
+
else:
|
|
509
|
+
left_cmp, right_cmp = left, right
|
|
510
|
+
try:
|
|
511
|
+
if not op(left_cmp, right_cmp):
|
|
512
|
+
return False
|
|
513
|
+
except TypeError as exc:
|
|
514
|
+
raise ExpressionError(f"cannot compare: {exc}") from exc
|
|
515
|
+
left = right
|
|
516
|
+
return True
|
|
517
|
+
|
|
518
|
+
if isinstance(node, ast.IfExp):
|
|
519
|
+
return self._eval(node.body, ctx) if self._eval(node.test, ctx) \
|
|
520
|
+
else self._eval(node.orelse, ctx)
|
|
521
|
+
|
|
522
|
+
if isinstance(node, ast.Attribute):
|
|
523
|
+
# `session.userId` e escrito como acesso a atributo, mas o
|
|
524
|
+
# contexto guarda a variavel de escopo sob a CHAVE PLANA
|
|
525
|
+
# pontilhada 'session.userId' — nao existe objeto `session`.
|
|
526
|
+
# Sem isto, uma variavel de sessao PRESENTE levantava, e quem
|
|
527
|
+
# chamava traduzia isso em '' ou em False: `<q:set
|
|
528
|
+
# value="{session.userId}"/>` gravava vazio num INSERT, e
|
|
529
|
+
# `<q:if condition="{session.userRole == 'admin'}">` era False
|
|
530
|
+
# nos dois ramos. A documentacao ensina exatamente esses dois
|
|
531
|
+
# padroes.
|
|
532
|
+
plano = self._dotted_name(node)
|
|
533
|
+
if plano is not None and plano in ctx:
|
|
534
|
+
return ctx[plano]
|
|
535
|
+
if (_ABSENT_SCOPE_KEY_IS_NONE.get() and isinstance(node.value, ast.Name)
|
|
536
|
+
and node.value.id in _SCOPES):
|
|
537
|
+
escopo = ctx.get(node.value.id)
|
|
538
|
+
if escopo is None or (isinstance(escopo, dict) and node.attr not in escopo):
|
|
539
|
+
return None
|
|
540
|
+
return self._attribute(self._eval(node.value, ctx), node.attr)
|
|
541
|
+
|
|
542
|
+
if isinstance(node, ast.Subscript):
|
|
543
|
+
target = self._eval(node.value, ctx)
|
|
544
|
+
key = self._eval(node.slice, ctx) if not isinstance(node.slice, ast.Slice) \
|
|
545
|
+
else slice(
|
|
546
|
+
self._eval(node.slice.lower, ctx) if node.slice.lower else None,
|
|
547
|
+
self._eval(node.slice.upper, ctx) if node.slice.upper else None,
|
|
548
|
+
)
|
|
549
|
+
try:
|
|
550
|
+
if isinstance(target, dict):
|
|
551
|
+
return target.get(key)
|
|
552
|
+
return target[key]
|
|
553
|
+
except (IndexError, KeyError) as exc:
|
|
554
|
+
raise UndefinedError(f"cannot index: {exc}") from exc
|
|
555
|
+
except TypeError as exc:
|
|
556
|
+
raise ExpressionError(f"cannot index: {exc}") from exc
|
|
557
|
+
|
|
558
|
+
if isinstance(node, ast.Dict):
|
|
559
|
+
return {self._eval(k, ctx): self._eval(v, ctx)
|
|
560
|
+
for k, v in zip(node.keys, node.values)}
|
|
561
|
+
|
|
562
|
+
if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
563
|
+
values = [self._eval(e, ctx) for e in node.elts]
|
|
564
|
+
return set(values) if isinstance(node, ast.Set) else \
|
|
565
|
+
(tuple(values) if isinstance(node, ast.Tuple) else values)
|
|
566
|
+
|
|
567
|
+
if isinstance(node, ast.Call):
|
|
568
|
+
return self._call(node, ctx)
|
|
569
|
+
|
|
570
|
+
raise ExpressionError(f"cannot evaluate {type(node).__name__}")
|
|
571
|
+
|
|
572
|
+
# -- helpers -----------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
@staticmethod
|
|
575
|
+
def _dotted_name(node: ast.Attribute):
|
|
576
|
+
"""'session.userId' de um Attribute, ou None se a base nao for um nome.
|
|
577
|
+
|
|
578
|
+
So encadeamento puro de nomes vira chave plana: `a.b.c` sim,
|
|
579
|
+
`f(x).b` ou `itens[0].nome` nao — nesses a busca por chave seria
|
|
580
|
+
errada, e o acesso a atributo de verdade e que vale.
|
|
581
|
+
"""
|
|
582
|
+
partes = []
|
|
583
|
+
atual = node
|
|
584
|
+
while isinstance(atual, ast.Attribute):
|
|
585
|
+
partes.append(atual.attr)
|
|
586
|
+
atual = atual.value
|
|
587
|
+
if not isinstance(atual, ast.Name):
|
|
588
|
+
return None
|
|
589
|
+
partes.append(atual.id)
|
|
590
|
+
return '.'.join(reversed(partes))
|
|
591
|
+
|
|
592
|
+
def _lookup(self, name: str, ctx: Dict[str, Any]) -> Any:
|
|
593
|
+
if name in ctx:
|
|
594
|
+
return ctx[name]
|
|
595
|
+
if name == 'true':
|
|
596
|
+
return True
|
|
597
|
+
if name == 'false':
|
|
598
|
+
return False
|
|
599
|
+
if name == 'null':
|
|
600
|
+
return None
|
|
601
|
+
raise UndefinedError(
|
|
602
|
+
f"variable {name!r} is not defined{_suggest(name, ctx)}"
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
def _attribute(self, target: Any, attr: str) -> Any:
|
|
606
|
+
if isinstance(target, dict):
|
|
607
|
+
if attr in target:
|
|
608
|
+
return target[attr]
|
|
609
|
+
raise UndefinedError(
|
|
610
|
+
f"key {attr!r} not found{_suggest(attr, target)}"
|
|
611
|
+
)
|
|
612
|
+
if isinstance(target, (list, tuple, str)) and attr == 'length':
|
|
613
|
+
return len(target)
|
|
614
|
+
if hasattr(target, attr):
|
|
615
|
+
valor = getattr(target, attr)
|
|
616
|
+
# Um atributo NUNCA pode entregar um callable.
|
|
617
|
+
#
|
|
618
|
+
# `_call` so aceita funcao com nome (bloqueia `x.metodo()`) e ha
|
|
619
|
+
# guarda de dunder sobre a AST — mas ambos eram contornados assim:
|
|
620
|
+
#
|
|
621
|
+
# max([alvo], key='{0.__class__.__base__}'.format)
|
|
622
|
+
#
|
|
623
|
+
# `'...'.format` e um ACESSO A ATRIBUTO que devolve um metodo
|
|
624
|
+
# ligado; passado como `key=` para uma funcao da stdlib, ela o
|
|
625
|
+
# INVOCA, e str.format faz travessia de atributo em tempo de
|
|
626
|
+
# execucao — onde a guarda de dunder, que olha o texto, nao
|
|
627
|
+
# alcanca. Reproduzido: a expressao acima executou uma @property
|
|
628
|
+
# do objeto do contexto.
|
|
629
|
+
#
|
|
630
|
+
# Um template le dado (`user.nome`), nao pega metodo para passar
|
|
631
|
+
# adiante. Recusar callable fecha a classe inteira de gadget, nao
|
|
632
|
+
# so o `format`.
|
|
633
|
+
if callable(valor) and not isinstance(valor, type):
|
|
634
|
+
raise ExpressionError(
|
|
635
|
+
f"attribute {attr!r} is a method, not data; expressions "
|
|
636
|
+
f"read values, they do not pass functions around"
|
|
637
|
+
)
|
|
638
|
+
return valor
|
|
639
|
+
if isinstance(target, (list, tuple)):
|
|
640
|
+
# The q:query case: `projects` is the rows and `projects_result`
|
|
641
|
+
# holds recordCount, so {projects.recordCount} is the natural
|
|
642
|
+
# first guess and it is wrong. Say so instead of going quiet.
|
|
643
|
+
raise ExpressionError(
|
|
644
|
+
f"{type(target).__name__} of {len(target)} items has no "
|
|
645
|
+
f"{attr!r}; use .length for the count, or the query's "
|
|
646
|
+
f"<name>_result for its metadata"
|
|
647
|
+
)
|
|
648
|
+
raise UndefinedError(
|
|
649
|
+
f"attribute {attr!r} not found on {type(target).__name__}"
|
|
650
|
+
f"{_suggest(attr, target)}"
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
def _call(self, node: ast.Call, ctx: Dict[str, Any]) -> Any:
|
|
654
|
+
if not isinstance(node.func, ast.Name):
|
|
655
|
+
raise ExpressionError("only named function calls are allowed" + _method_hint(node.func))
|
|
656
|
+
fn = self.functions.get(node.func.id)
|
|
657
|
+
if fn is None and self.function_resolver is not None:
|
|
658
|
+
# A q:function declared by the component being executed.
|
|
659
|
+
# {countElements(myArray)} used to resolve to nothing at all: the
|
|
660
|
+
# placeholder was left in the text and the surrounding q:set then
|
|
661
|
+
# failed with "could not convert string to float:
|
|
662
|
+
# '{sumNumbers(myNumbers)}'".
|
|
663
|
+
fn = self.function_resolver(node.func.id)
|
|
664
|
+
if fn is None:
|
|
665
|
+
raise ExpressionError(
|
|
666
|
+
f"function {node.func.id!r} is not defined"
|
|
667
|
+
+ (f"; write {_JS_EQUIVALENTS[node.func.id]}" if node.func.id in _JS_EQUIVALENTS
|
|
668
|
+
else _suggest(node.func.id, self.functions))
|
|
669
|
+
)
|
|
670
|
+
args = [self._eval(a, ctx) for a in node.args]
|
|
671
|
+
kwargs = {kw.arg: self._eval(kw.value, ctx) for kw in node.keywords}
|
|
672
|
+
try:
|
|
673
|
+
return fn(*args, **kwargs)
|
|
674
|
+
except ExpressionError:
|
|
675
|
+
raise
|
|
676
|
+
except Exception as exc: # noqa: BLE001
|
|
677
|
+
raise ExpressionError(f"{node.func.id}() failed: {exc}") from exc
|