tina4-python 3.13.86__tar.gz → 3.13.87__tar.gz
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.
- {tina4_python-3.13.86 → tina4_python-3.13.87}/PKG-INFO +1 -1
- {tina4_python-3.13.86 → tina4_python-3.13.87}/pyproject.toml +1 -1
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/CLAUDE.md +1 -1
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/cli/__init__.py +2 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dev_admin/metrics.py +11 -2
- tina4_python-3.13.87/tina4_python/frond/compiler.py +259 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/frond/engine.py +475 -646
- tina4_python-3.13.87/tina4_python/frond/parser.py +745 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/.gitignore +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/README.md +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/env.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mqtt/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/mqtt/message.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_pagination.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.86 → tina4_python-3.13.87}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1913,7 +1913,7 @@ async def dashboard(request, response):
|
|
|
1913
1913
|
## v3 Features Summary
|
|
1914
1914
|
|
|
1915
1915
|
- **98 built-in features**, zero third-party dependencies
|
|
1916
|
-
- **
|
|
1916
|
+
- **3,887 tests** passing across all modules
|
|
1917
1917
|
- **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
|
|
1918
1918
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
1919
1919
|
- **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), DB query caching — request-scoped auto cache **off by default — opt-in via `TINA4_AUTO_CACHING=true`** (TTL `TINA4_AUTO_CACHING_TTL=5`s) dedupes identical reads within a request and flushes on writes; it ships OFF because a request-scoped cache can return pre-write state in a read-after-write (`SELECT MAX(id)` before an `INSERT` → duplicate keys), so opt in per read-heavy endpoint. Persistent cross-request cache also opt-in via `TINA4_DB_CACHE=true` (TTL `TINA4_DB_CACHE_TTL=30`s) routed through the unified backend set via `TINA4_DB_CACHE_BACKEND` (memory/file/redis/valkey/memcached/mongodb/database) + `TINA4_DB_CACHE_URL` so instances share one cache with global write-invalidation; `cache_stats()` reports `mode` (request/persistent/off) and `backend`, `cache_clear()`
|
|
@@ -787,6 +787,8 @@ def _init(args):
|
|
|
787
787
|
print(f"\nProject scaffolded at {target.resolve()}")
|
|
788
788
|
print(" Run: tina4python serve")
|
|
789
789
|
print(" Run: tina4python ai (detect & install AI tool context)")
|
|
790
|
+
print(" Then: open the dev admin at /__dev, paste your Tina4 MCP key,")
|
|
791
|
+
print(" and describe what to build. See https://tina4.com/build-with-ai")
|
|
790
792
|
|
|
791
793
|
|
|
792
794
|
# ── Serve ─────────────────────────────────────────────────────────────
|
|
@@ -323,7 +323,14 @@ def full_analysis(root: str = "src") -> dict:
|
|
|
323
323
|
"total_functions": len(all_functions),
|
|
324
324
|
"avg_complexity": round(avg_cc, 2),
|
|
325
325
|
"avg_maintainability": round(avg_mi, 1),
|
|
326
|
+
# Display-only: the top-15 for the "most complex functions" report.
|
|
327
|
+
# Do NOT source offenders / --fail-on from this — capping here silently
|
|
328
|
+
# hides the 16th+ over-threshold function from the gate. offenders()
|
|
329
|
+
# reads "all_functions" (below) instead.
|
|
326
330
|
"most_complex_functions": all_functions[:15],
|
|
331
|
+
# Full, uncapped, complexity-sorted list — offenders()/--fail-on use this
|
|
332
|
+
# so no function over the complexity threshold ever escapes the gate.
|
|
333
|
+
"all_functions": all_functions,
|
|
327
334
|
"file_metrics": file_metrics,
|
|
328
335
|
"violations": violations,
|
|
329
336
|
"dependency_graph": import_graph,
|
|
@@ -369,8 +376,10 @@ def offenders(root: str = "src", top: int = 20) -> dict:
|
|
|
369
376
|
|
|
370
377
|
items: list[dict] = []
|
|
371
378
|
|
|
372
|
-
# Function-level: cyclomatic complexity.
|
|
373
|
-
|
|
379
|
+
# Function-level: cyclomatic complexity. Use the FULL function list (not the
|
|
380
|
+
# display-capped most_complex_functions[:15]) so a 16th+ over-threshold
|
|
381
|
+
# function is never silently dropped from the offenders list or --fail-on.
|
|
382
|
+
for fn in analysis.get("all_functions", analysis.get("most_complex_functions", [])):
|
|
374
383
|
cc = fn["complexity"]
|
|
375
384
|
if cc > 10:
|
|
376
385
|
items.append({
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Tina4 Frond — Ahead-of-time template compiler (ADR-0001, part 1).
|
|
2
|
+
"""
|
|
3
|
+
Compile a parsed Frond AST ONCE into a native Python callable so that
|
|
4
|
+
``render()`` just CALLS the compiled function and pays no per-render tree-walk.
|
|
5
|
+
|
|
6
|
+
Design (behaviour-safe by construction):
|
|
7
|
+
|
|
8
|
+
* The compiler consumes the SAME tree the interpreter does, produced by
|
|
9
|
+
:mod:`tina4_python.frond.parser`. It no longer sees tokens, so it cannot
|
|
10
|
+
disagree with the interpreter about structure: which nodes belong to which
|
|
11
|
+
branch/body, where a loop ends, and where whitespace is trimmed are all
|
|
12
|
+
decided once, by the parser. (This module used to carry ``_collect_if`` /
|
|
13
|
+
``_collect_for`` — a second, hand-synchronised copy of the engine's own token
|
|
14
|
+
grouping — plus ``_apply_whitespace_control``. All three are deleted; the
|
|
15
|
+
parser owns that work now.)
|
|
16
|
+
* The emitted function reuses the engine's OWN runtime primitives for every
|
|
17
|
+
value-producing operation — ``engine._eval_var`` for ``{{ ... }}``,
|
|
18
|
+
``_eval_comparison`` for ``if`` conditions, ``_eval_expr`` for ``for``
|
|
19
|
+
iterables, ``engine._handle_set`` for ``set``. Only the *structure* (which
|
|
20
|
+
nodes belong to which branch/body, the whitespace-free text runs, the loop
|
|
21
|
+
bookkeeping) is baked into native Python control flow. Because every value is
|
|
22
|
+
produced by the identical primitive the interpreter calls, a compiled template
|
|
23
|
+
renders byte-identically to the interpreted one.
|
|
24
|
+
* Only the common hot-path constructs are compiled: text, ``{{ var }}`` (with
|
|
25
|
+
dotted paths / filters / ternary — all handled inside ``_eval_var``),
|
|
26
|
+
``if/elif/elseif/else``, ``for`` (+ loop vars + for-else), ``set``, comments,
|
|
27
|
+
and ``{% raw %}`` (which the tokenizer has already turned into literal text).
|
|
28
|
+
* Whitespace control is compiled too, because the parser baked the structural
|
|
29
|
+
trims into the text nodes. The ONE case that stays render-dependent is a
|
|
30
|
+
strip-before (``{{- `` / ``{%- ``) whose left neighbour is not literal text
|
|
31
|
+
(a value / block / comment output, mid-body): there the interpreter
|
|
32
|
+
right-strips the live output buffer, so the parser flags that node with
|
|
33
|
+
``strip_before`` and this module falls back on it.
|
|
34
|
+
* ANY other construct (extends/block, include, macro, from/import, cache,
|
|
35
|
+
spaceless, autoescape, live) or a malformed tag makes :func:`compile_template`
|
|
36
|
+
return ``None`` so the caller falls back to the interpreter for that whole
|
|
37
|
+
template. A codegen/compile error also returns ``None``. A render is therefore
|
|
38
|
+
never broken by the compiler.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _Unsupported(Exception):
|
|
43
|
+
"""Raised internally when a node uses a construct the compiler does not
|
|
44
|
+
yet emit. Caught by :func:`compile_template`, which then returns ``None``
|
|
45
|
+
so the caller uses the interpreted renderer for the whole template."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _tostr(value) -> str:
|
|
49
|
+
"""Coerce a rendered value to its output string.
|
|
50
|
+
|
|
51
|
+
THE cross-framework output contract, and it MUST stay identical to the
|
|
52
|
+
interpreter's copy in ``engine.Frond._to_output`` -- the compiled path and the
|
|
53
|
+
interpreted path have to render the same bytes. Change one, change both.
|
|
54
|
+
|
|
55
|
+
A boolean renders lowercase ``true``/``false``: Frond is a template language,
|
|
56
|
+
not Python, and lowercase is the form usable directly in HTML and JavaScript
|
|
57
|
+
(``data-active="{{ flag }}"`` -> ``data-active="true"``, testable from JS).
|
|
58
|
+
|
|
59
|
+
Breaking in 3.13.87: this used to emit Python's ``True``/``False``. The four
|
|
60
|
+
frameworks had drifted to four different answers -- Python ``True``/``False``
|
|
61
|
+
(Jinja2-faithful), PHP ``1``/``''`` (Twig-faithful, with ``false`` rendering as
|
|
62
|
+
an EMPTY STRING), Ruby inconsistent between a comparison and a bare variable,
|
|
63
|
+
Node ``true``/``false``. All four now agree; a 72-expression corpus locks it.
|
|
64
|
+
|
|
65
|
+
``is True`` / ``is False`` identity checks are deliberate: ``1 == True`` in
|
|
66
|
+
Python, and an integer 1 must still render as ``1``.
|
|
67
|
+
"""
|
|
68
|
+
if value is None:
|
|
69
|
+
return ""
|
|
70
|
+
if value is True:
|
|
71
|
+
return "true"
|
|
72
|
+
if value is False:
|
|
73
|
+
return "false"
|
|
74
|
+
return str(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _pad(indent: int) -> str:
|
|
78
|
+
return " " * indent
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def compile_template(ast):
|
|
82
|
+
"""Compile an AST (a list of nodes) to ``_rendered(engine, ctx) -> str``.
|
|
83
|
+
|
|
84
|
+
Returns the callable, or ``None`` when the template contains a construct
|
|
85
|
+
that is not compiled yet / when codegen or ``compile()`` fails — in which
|
|
86
|
+
case the caller must render the template with the interpreter.
|
|
87
|
+
"""
|
|
88
|
+
# Lazy import breaks the engine<->compiler import cycle (each imports the
|
|
89
|
+
# other only at call time, by which point both modules are loaded).
|
|
90
|
+
from tina4_python.frond import engine as _engine
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
lines = ["def _rendered(engine, ctx):", " _b = []", " _ap = _b.append"]
|
|
94
|
+
_emit_body(ast, "ctx", 1, lines, {"n": 0})
|
|
95
|
+
lines.append(' return "".join(_b)')
|
|
96
|
+
source = "\n".join(lines)
|
|
97
|
+
|
|
98
|
+
namespace = {
|
|
99
|
+
"_LoopContext": _engine._LoopContext,
|
|
100
|
+
"_eval_expr": _engine._eval_expr,
|
|
101
|
+
"_eval_comparison": _engine._eval_comparison,
|
|
102
|
+
"_tostr": _tostr,
|
|
103
|
+
"isinstance": isinstance,
|
|
104
|
+
"list": list,
|
|
105
|
+
"len": len,
|
|
106
|
+
"enumerate": enumerate,
|
|
107
|
+
}
|
|
108
|
+
code = compile(source, "<frond_compiled>", "exec")
|
|
109
|
+
exec(code, namespace) # noqa: S102 — trusted, generated from template nodes
|
|
110
|
+
return namespace["_rendered"]
|
|
111
|
+
except _Unsupported:
|
|
112
|
+
return None
|
|
113
|
+
except Exception:
|
|
114
|
+
# A codegen/compile error must never break a render — fall back.
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ── Codegen ─────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _emit_body(nodes, ctxvar, indent, out, state):
|
|
122
|
+
"""Emit code for a list of sibling nodes (mirrors ``_render_nodes`` dispatch
|
|
123
|
+
for the compiled subset). ``ctxvar`` is the name of the context variable in
|
|
124
|
+
scope (``ctx`` at top level, a loop-context name inside a ``for``)."""
|
|
125
|
+
for node in nodes:
|
|
126
|
+
kind = node.kind
|
|
127
|
+
|
|
128
|
+
if kind == "text":
|
|
129
|
+
out.append(_pad(indent) + "_ap(" + repr(node.text) + ")")
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if kind == "comment":
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# A strip-before the parser could not bake into a text node: the
|
|
136
|
+
# interpreter right-strips the LIVE output buffer there, which depends on
|
|
137
|
+
# rendered data, so the whole template falls back.
|
|
138
|
+
if node.strip_before:
|
|
139
|
+
raise _Unsupported
|
|
140
|
+
|
|
141
|
+
if kind == "output":
|
|
142
|
+
out.append(
|
|
143
|
+
_pad(indent)
|
|
144
|
+
+ "_ap(_tostr(engine._eval_var(" + repr(node.expr) + ", " + ctxvar + ")))"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
elif kind == "if":
|
|
148
|
+
_emit_if(node, ctxvar, indent, out, state)
|
|
149
|
+
|
|
150
|
+
elif kind == "for":
|
|
151
|
+
_emit_for(node, ctxvar, indent, out, state)
|
|
152
|
+
|
|
153
|
+
elif kind == "set":
|
|
154
|
+
out.append(
|
|
155
|
+
_pad(indent) + "engine._handle_set(" + repr(node.content) + ", " + ctxvar + ")"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
else:
|
|
159
|
+
# extends/block, include, macro, from/import, cache, spaceless,
|
|
160
|
+
# autoescape, live, and any stray terminator: not compiled — fall
|
|
161
|
+
# back to the interpreter for this template.
|
|
162
|
+
raise _Unsupported
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _emit_if(node, ctxvar, indent, out, state):
|
|
166
|
+
branches = node.branches
|
|
167
|
+
if not branches:
|
|
168
|
+
raise _Unsupported
|
|
169
|
+
|
|
170
|
+
# A ``None`` condition is the ``{% else %}`` branch; native ``else:`` must be
|
|
171
|
+
# last. Anything else (an else before an elseif) is malformed — fall back.
|
|
172
|
+
for idx, (cond, _body) in enumerate(branches):
|
|
173
|
+
if cond is None and idx != len(branches) - 1:
|
|
174
|
+
raise _Unsupported
|
|
175
|
+
|
|
176
|
+
first = True
|
|
177
|
+
for cond, body in branches:
|
|
178
|
+
if cond is None:
|
|
179
|
+
out.append(_pad(indent) + "else:")
|
|
180
|
+
else:
|
|
181
|
+
keyword = "if" if first else "elif"
|
|
182
|
+
out.append(
|
|
183
|
+
_pad(indent)
|
|
184
|
+
+ keyword
|
|
185
|
+
+ " _eval_comparison("
|
|
186
|
+
+ repr(cond)
|
|
187
|
+
+ ", "
|
|
188
|
+
+ ctxvar
|
|
189
|
+
+ ", engine._eval_var_raw):"
|
|
190
|
+
)
|
|
191
|
+
body_start = len(out)
|
|
192
|
+
_emit_body(body, ctxvar, indent + 1, out, state)
|
|
193
|
+
if len(out) == body_start:
|
|
194
|
+
out.append(_pad(indent + 1) + "pass")
|
|
195
|
+
first = False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _emit_for(node, ctxvar, indent, out, state):
|
|
199
|
+
var1 = node.key_var
|
|
200
|
+
var2 = node.value_var
|
|
201
|
+
|
|
202
|
+
suffix = state["n"]
|
|
203
|
+
state["n"] += 1
|
|
204
|
+
it = "_it%d" % suffix
|
|
205
|
+
items = "_items%d" % suffix
|
|
206
|
+
total = "_total%d" % suffix
|
|
207
|
+
isd = "_isd%d" % suffix
|
|
208
|
+
idx = "_idx%d" % suffix
|
|
209
|
+
item = "_item%d" % suffix
|
|
210
|
+
lc = "_lc%d" % suffix
|
|
211
|
+
k = "_k%d" % suffix
|
|
212
|
+
v = "_v%d" % suffix
|
|
213
|
+
|
|
214
|
+
out.append(_pad(indent) + it + " = _eval_expr(" + repr(node.iterable) + ", " + ctxvar + ")")
|
|
215
|
+
out.append(_pad(indent) + "if not " + it + ":")
|
|
216
|
+
body_start = len(out)
|
|
217
|
+
if node.else_body:
|
|
218
|
+
_emit_body(node.else_body, ctxvar, indent + 1, out, state)
|
|
219
|
+
if len(out) == body_start:
|
|
220
|
+
out.append(_pad(indent + 1) + "pass")
|
|
221
|
+
|
|
222
|
+
out.append(_pad(indent) + "else:")
|
|
223
|
+
out.append(
|
|
224
|
+
_pad(indent + 1)
|
|
225
|
+
+ items + " = list(" + it + ".items()) if isinstance(" + it + ", dict) else list(" + it + ")"
|
|
226
|
+
)
|
|
227
|
+
out.append(_pad(indent + 1) + total + " = len(" + items + ")")
|
|
228
|
+
out.append(_pad(indent + 1) + isd + " = isinstance(" + it + ", dict)")
|
|
229
|
+
out.append(_pad(indent + 1) + "for " + idx + ", " + item + " in enumerate(" + items + "):")
|
|
230
|
+
out.append(_pad(indent + 2) + lc + " = _LoopContext(" + ctxvar + ")")
|
|
231
|
+
loop_dict = (
|
|
232
|
+
'{"index": ' + idx + "+1, "
|
|
233
|
+
'"index0": ' + idx + ", "
|
|
234
|
+
'"first": ' + idx + "==0, "
|
|
235
|
+
'"last": ' + idx + "==" + total + "-1, "
|
|
236
|
+
'"length": ' + total + ", "
|
|
237
|
+
'"revindex": ' + total + "-" + idx + ", "
|
|
238
|
+
'"revindex0": ' + total + "-" + idx + "-1, "
|
|
239
|
+
'"even": (' + idx + "+1)%2==0, "
|
|
240
|
+
'"odd": (' + idx + "+1)%2!=0}"
|
|
241
|
+
)
|
|
242
|
+
out.append(_pad(indent + 2) + lc + '["loop"] = ' + loop_dict)
|
|
243
|
+
|
|
244
|
+
out.append(_pad(indent + 2) + "if " + isd + ":")
|
|
245
|
+
out.append(_pad(indent + 3) + k + ", " + v + " = " + item)
|
|
246
|
+
out.append(_pad(indent + 3) + lc + "[" + repr(var1) + "] = " + k)
|
|
247
|
+
if var2:
|
|
248
|
+
out.append(_pad(indent + 3) + lc + "[" + repr(var2) + "] = " + v)
|
|
249
|
+
out.append(_pad(indent + 2) + "else:")
|
|
250
|
+
if var2:
|
|
251
|
+
out.append(_pad(indent + 3) + lc + "[" + repr(var1) + "] = " + idx)
|
|
252
|
+
out.append(_pad(indent + 3) + lc + "[" + repr(var2) + "] = " + item)
|
|
253
|
+
else:
|
|
254
|
+
out.append(_pad(indent + 3) + lc + "[" + repr(var1) + "] = " + item)
|
|
255
|
+
|
|
256
|
+
body_start = len(out)
|
|
257
|
+
_emit_body(node.body, lc, indent + 2, out, state)
|
|
258
|
+
if len(out) == body_start:
|
|
259
|
+
out.append(_pad(indent + 2) + "pass")
|