tina4-python 3.12.13__tar.gz → 3.12.14__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.
Files changed (156) hide show
  1. {tina4_python-3.12.13 → tina4_python-3.12.14}/PKG-INFO +1 -1
  2. {tina4_python-3.12.13 → tina4_python-3.12.14}/pyproject.toml +1 -1
  3. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/__init__.py +1 -1
  4. tina4_python-3.12.14/tina4_python/test/__init__.py +389 -0
  5. {tina4_python-3.12.13 → tina4_python-3.12.14}/.gitignore +0 -0
  6. {tina4_python-3.12.13 → tina4_python-3.12.14}/README.md +0 -0
  7. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/ai/__init__.py +0 -0
  11. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/__init__.py +0 -0
  17. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/cache.py +0 -0
  18. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/constants.py +0 -0
  19. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/events.py +0 -0
  20. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/middleware.py +0 -0
  21. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/rate_limiter.py +0 -0
  22. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/request.py +0 -0
  23. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/response.py +0 -0
  24. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/router.py +0 -0
  25. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/core/server.py +0 -0
  26. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/mongodb.py +0 -0
  32. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/postgres.py +0 -0
  36. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/dev_admin/__init__.py +0 -0
  40. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/dev_admin/metrics.py +0 -0
  41. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/dev_admin/plan.py +0 -0
  42. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/dev_admin/project_index.py +0 -0
  43. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/docs.py +0 -0
  44. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/dotenv/__init__.py +0 -0
  45. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/frond/FROND.md +0 -0
  46. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/frond/__init__.py +0 -0
  47. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/frond/engine.py +0 -0
  48. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/auth/meta.json +0 -0
  49. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  50. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/database/meta.json +0 -0
  51. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  52. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/error-overlay/meta.json +0 -0
  53. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  54. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/orm/meta.json +0 -0
  55. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  56. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  57. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/queue/meta.json +0 -0
  58. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  59. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/rest-api/meta.json +0 -0
  60. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  61. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/templates/meta.json +0 -0
  62. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  63. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  64. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/graphql/__init__.py +0 -0
  65. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/i18n/__init__.py +0 -0
  66. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/mcp/__init__.py +0 -0
  67. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/mcp/protocol.py +0 -0
  68. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/mcp/tools.py +0 -0
  69. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/messenger/__init__.py +0 -0
  70. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/migration/__init__.py +0 -0
  71. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/migration/runner.py +0 -0
  72. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/orm/__init__.py +0 -0
  73. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/orm/fields.py +0 -0
  74. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/orm/model.py +0 -0
  75. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/test_client/__init__.py +0 -0
  141. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  142. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  143. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  148. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  149. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  150. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  151. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  152. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  153. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/validator/__init__.py +0 -0
  154. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/websocket/__init__.py +0 -0
  155. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/websocket/backplane.py +0 -0
  156. {tina4_python-3.12.13 → tina4_python-3.12.14}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.12.13
3
+ Version: 3.12.14
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.12.13"
3
+ version = "3.12.14"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.12.13"
11
+ __version__ = "3.12.14"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -0,0 +1,389 @@
1
+ # Tina4 xUnit-Style Testing Surface
2
+ """
3
+ Class-based testing with positional assertions. This module exists so the
4
+ following pattern — used throughout the testing documentation — actually
5
+ runs::
6
+
7
+ from tina4_python.test import Test, assert_equal, assert_true
8
+
9
+ class BasicTest(Test):
10
+
11
+ def test_addition(self):
12
+ assert_equal(2 + 2, 4, "Basic addition should work")
13
+
14
+ def test_string_contains(self):
15
+ greeting = "Hello, World!"
16
+ assert_true("World" in greeting, "Greeting should contain 'World'")
17
+
18
+ The `Test` base class inherits from ``unittest.TestCase``. Pytest discovers
19
+ any class that inherits from ``TestCase`` regardless of the class-name
20
+ convention, so class names do NOT need to start with ``Test``. ``BasicTest``,
21
+ ``UserCRUDTest``, anything works.
22
+
23
+ The assertion functions take **positional** arguments: ``(actual, expected,
24
+ message)``. This is deliberately different from the inline ``@tests``
25
+ decorator in ``tina4_python.Testing``, which uses a tuple form
26
+ ``assert_equal((args,), expected)``. The two surfaces serve different
27
+ purposes:
28
+
29
+ - ``tina4_python.test`` — class-based suites, run with ``tina4 test`` or pytest
30
+ - ``tina4_python.Testing`` — inline assertions next to the function under test
31
+
32
+ Run any test with ``tina4 test`` or ``.venv/bin/python -m pytest tests/``.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import unittest
37
+ from typing import Any, Callable
38
+
39
+
40
+ class Test(unittest.TestCase):
41
+ """Base class for Tina4 xUnit-style test suites.
42
+
43
+ Inherits from ``unittest.TestCase`` so pytest discovers any subclass by
44
+ inheritance — class names do not need to start with ``Test``.
45
+
46
+ **Lifecycle hooks** — Tina4 prefers snake_case names that match Python
47
+ style. Override either form; both are called for you::
48
+
49
+ class UserTest(Test):
50
+ def set_up(self): # snake_case — Tina4 idiom
51
+ self.user = User.create({...})
52
+
53
+ def tear_down(self): # snake_case — Tina4 idiom
54
+ self.user.delete()
55
+
56
+ The classic ``unittest`` ``setUp`` / ``tearDown`` still works for users
57
+ coming from ``unittest.TestCase``; you may use either, but do not mix
58
+ the two on the same class.
59
+ """
60
+
61
+ # ── Lifecycle adapters ──────────────────────────────────────────────
62
+ #
63
+ # unittest.TestCase only calls setUp / tearDown (camelCase). Tina4
64
+ # documents set_up / tear_down (snake_case). Bridge here so the
65
+ # snake_case methods get called when subclasses override them.
66
+
67
+ def setUp(self) -> None: # noqa: N802 — unittest convention
68
+ super().setUp()
69
+ # Only call set_up if a subclass defined it (avoids double-calling
70
+ # if the user uses unittest-style setUp instead).
71
+ if type(self).set_up is not Test.set_up:
72
+ self.set_up()
73
+
74
+ def tearDown(self) -> None: # noqa: N802 — unittest convention
75
+ if type(self).tear_down is not Test.tear_down:
76
+ self.tear_down()
77
+ super().tearDown()
78
+
79
+ def set_up(self) -> None:
80
+ """Snake-case lifecycle hook — runs before each test method.
81
+
82
+ Default no-op. Override to set up per-test fixtures.
83
+ """
84
+
85
+ def tear_down(self) -> None:
86
+ """Snake-case lifecycle hook — runs after each test method,
87
+ regardless of pass or fail.
88
+
89
+ Default no-op. Override to clean up per-test state.
90
+ """
91
+
92
+
93
+ # ── Assertions ──────────────────────────────────────────────────────────
94
+ #
95
+ # Each assertion follows the same shape: positional value(s) + optional
96
+ # message. They raise ``AssertionError`` on failure, which pytest and the
97
+ # ``tina4 test`` CLI both render with the trailing message.
98
+
99
+
100
+ # ── Argument-shape helper ───────────────────────────────────────────────
101
+ #
102
+ # Every assertion below accepts a UNIFORM (actual, expected, message)
103
+ # signature so the API reads the same regardless of which assertion you
104
+ # pick. For binary assertions (assert_true, assert_false, assert_none,
105
+ # assert_not_none) the ``expected`` slot is redundant — it states the
106
+ # obvious — but spelling it out keeps the call shape consistent with
107
+ # assert_equal / assert_not_equal where it carries real information.
108
+ #
109
+ # Two-argument legacy calls (value, message) are still accepted because
110
+ # the chapter's examples shipped that way for years. The helper detects
111
+ # the form by argument type:
112
+ # assert_false(condition, "message") → 2-arg legacy form
113
+ # assert_false(condition, False, "message") → 3-arg uniform form
114
+ # When the second positional arg is a string AND no third arg was given,
115
+ # we treat it as the message.
116
+
117
+ _SENTINEL = object()
118
+
119
+
120
+ def _split_expected_message(
121
+ expected_or_message: Any, message: str, default_expected: Any
122
+ ) -> tuple[Any, str]:
123
+ """Split a 2-arg vs 3-arg call into (expected, message)."""
124
+ if message:
125
+ # Caller passed all three positional args — honour them as-given.
126
+ return expected_or_message, message
127
+ if expected_or_message is _SENTINEL:
128
+ # Caller passed only the first positional — no message, no expected.
129
+ return default_expected, ""
130
+ if isinstance(expected_or_message, str):
131
+ # Two-arg legacy form: the second arg is the message.
132
+ return default_expected, expected_or_message
133
+ # Two-arg uniform form: second arg is the expected value, no message.
134
+ return expected_or_message, ""
135
+
136
+
137
+ def assert_equal(actual: Any, expected: Any, message: str = "") -> None:
138
+ """Assert ``actual == expected``.
139
+
140
+ Signature: ``assert_equal(actual, expected, message="")``
141
+ """
142
+ if actual != expected:
143
+ raise AssertionError(
144
+ message or f"Expected {expected!r}, got {actual!r}"
145
+ )
146
+
147
+
148
+ def assert_not_equal(actual: Any, expected: Any, message: str = "") -> None:
149
+ """Assert ``actual != expected``.
150
+
151
+ Signature: ``assert_not_equal(actual, expected, message="")``
152
+ """
153
+ if actual == expected:
154
+ raise AssertionError(
155
+ message or f"Expected {actual!r} != {expected!r}, but they are equal"
156
+ )
157
+
158
+
159
+ def assert_true(actual: Any, expected: Any = _SENTINEL, message: str = "") -> None:
160
+ """Assert ``actual`` is truthy.
161
+
162
+ Signature: ``assert_true(actual, expected=True, message="")``
163
+
164
+ The ``expected`` slot is there for parity with ``assert_equal`` — pass
165
+ ``True`` to spell it out, or omit it and pass the message directly.
166
+ Both forms work::
167
+
168
+ assert_true(x, "should be truthy") # legacy
169
+ assert_true(x, True, "should be truthy") # uniform
170
+ """
171
+ expected, message = _split_expected_message(expected, message, True)
172
+ if not actual:
173
+ raise AssertionError(
174
+ message or f"Expected truthy ({expected!r}), got {actual!r}"
175
+ )
176
+
177
+
178
+ def assert_false(actual: Any, expected: Any = _SENTINEL, message: str = "") -> None:
179
+ """Assert ``actual`` is falsy.
180
+
181
+ Signature: ``assert_false(actual, expected=False, message="")``
182
+
183
+ The ``expected`` slot is there for parity with ``assert_equal`` — pass
184
+ ``False`` to spell it out, or omit it and pass the message directly.
185
+ Both forms work::
186
+
187
+ assert_false(x, "should be falsy") # legacy
188
+ assert_false(x, False, "should be falsy") # uniform
189
+ """
190
+ expected, message = _split_expected_message(expected, message, False)
191
+ if actual:
192
+ raise AssertionError(
193
+ message or f"Expected falsy ({expected!r}), got {actual!r}"
194
+ )
195
+
196
+
197
+ def assert_none(value: Any, expected: Any = _SENTINEL, message: str = "") -> None:
198
+ """Assert ``value`` is ``None``.
199
+
200
+ Signature: ``assert_none(actual, expected=None, message="")``
201
+ """
202
+ expected, message = _split_expected_message(expected, message, None)
203
+ if value is not None:
204
+ raise AssertionError(
205
+ message or f"Expected None, got {value!r}"
206
+ )
207
+
208
+
209
+ def assert_not_none(value: Any, expected: Any = _SENTINEL, message: str = "") -> None:
210
+ """Assert ``value`` is not ``None``.
211
+
212
+ Signature: ``assert_not_none(actual, expected="not None", message="")``
213
+ """
214
+ expected, message = _split_expected_message(expected, message, "not None")
215
+ if value is None:
216
+ raise AssertionError(message or "Expected not None, got None")
217
+
218
+
219
+ def assert_in(item: Any, container: Any, message: str = "") -> None:
220
+ """Assert ``item`` is contained in ``container``.
221
+
222
+ Signature: ``assert_in(item, container, message="")``
223
+ """
224
+ if item not in container:
225
+ raise AssertionError(
226
+ message or f"Expected {item!r} in {container!r}"
227
+ )
228
+
229
+
230
+ def assert_not_in(item: Any, container: Any, message: str = "") -> None:
231
+ """Assert ``item`` is not in ``container``.
232
+
233
+ Signature: ``assert_not_in(item, container, message="")``
234
+ """
235
+ if item in container:
236
+ raise AssertionError(
237
+ message or f"Expected {item!r} not in {container!r}"
238
+ )
239
+
240
+
241
+ def assert_is_instance(value: Any, expected_type: type, message: str = "") -> None:
242
+ """Assert ``value`` is an instance of ``expected_type``.
243
+
244
+ Signature: ``assert_is_instance(value, expected_type, message="")``
245
+ """
246
+ if not isinstance(value, expected_type):
247
+ raise AssertionError(
248
+ message
249
+ or f"Expected instance of {expected_type.__name__}, got {type(value).__name__}"
250
+ )
251
+
252
+
253
+ def assert_greater(actual: Any, expected: Any, message: str = "") -> None:
254
+ """Assert ``actual > expected``.
255
+
256
+ Signature: ``assert_greater(actual, expected, message="")``
257
+ """
258
+ if not (actual > expected):
259
+ raise AssertionError(message or f"Expected {actual!r} > {expected!r}")
260
+
261
+
262
+ def assert_less(actual: Any, expected: Any, message: str = "") -> None:
263
+ """Assert ``actual < expected``.
264
+
265
+ Signature: ``assert_less(actual, expected, message="")``
266
+ """
267
+ if not (actual < expected):
268
+ raise AssertionError(message or f"Expected {actual!r} < {expected!r}")
269
+
270
+
271
+ def assert_almost_equal(
272
+ actual: float, expected: float, places: int = 7, message: str = ""
273
+ ) -> None:
274
+ """Assert two floats are equal to ``places`` decimal places.
275
+
276
+ Signature: ``assert_almost_equal(actual, expected, places=7, message="")``
277
+
278
+ Useful for floating-point comparisons where exact equality is unreliable.
279
+ """
280
+ if round(abs(actual - expected), places) != 0:
281
+ raise AssertionError(
282
+ message
283
+ or f"Expected {expected!r} ≈ {actual!r} (within {places} places)"
284
+ )
285
+
286
+
287
+ def assert_raises(
288
+ callable_or_exception: Callable | type,
289
+ exception_class: type | None = None,
290
+ message: str = "",
291
+ ):
292
+ """Assert that a callable raises a specific exception.
293
+
294
+ Three forms, all supported::
295
+
296
+ # 1. Docs form — callable first, exception second, optional message
297
+ assert_raises(lambda: int("not a number"), ValueError, "must raise")
298
+
299
+ # 2. Context manager — useful when the body is more than one line
300
+ with assert_raises(ValueError):
301
+ value = parse(input)
302
+ process(value)
303
+
304
+ # 3. unittest.TestCase compatibility — exception first form
305
+ assert_raises(ValueError, lambda: int("not a number"))
306
+
307
+ Raises ``AssertionError`` if no exception is raised, or if a different
308
+ exception type is raised.
309
+ """
310
+ # Form 2: context manager — caller passed just the exception class
311
+ if exception_class is None:
312
+ if not isinstance(callable_or_exception, type) or not issubclass(
313
+ callable_or_exception, BaseException
314
+ ):
315
+ raise TypeError(
316
+ "assert_raises with one argument requires an exception class"
317
+ )
318
+ return _RaisesContext(callable_or_exception)
319
+
320
+ # Determine which argument is the callable and which is the exception
321
+ # by type — supports docs form AND unittest order without ambiguity.
322
+ if isinstance(callable_or_exception, type) and issubclass(
323
+ callable_or_exception, BaseException
324
+ ):
325
+ # Form 3: assert_raises(ExceptionType, callable)
326
+ exc_type = callable_or_exception
327
+ fn = exception_class
328
+ else:
329
+ # Form 1: assert_raises(callable, ExceptionType, message)
330
+ fn = callable_or_exception
331
+ exc_type = exception_class
332
+
333
+ if not callable(fn):
334
+ raise TypeError("assert_raises requires a callable")
335
+
336
+ try:
337
+ fn()
338
+ except exc_type:
339
+ return
340
+ except Exception as e:
341
+ raise AssertionError(
342
+ message
343
+ or f"Expected {exc_type.__name__}, got {type(e).__name__}: {e}"
344
+ )
345
+ raise AssertionError(
346
+ message
347
+ or f"Expected {exc_type.__name__} to be raised, but nothing was"
348
+ )
349
+
350
+
351
+ class _RaisesContext:
352
+ """Context-manager form of ``assert_raises``."""
353
+
354
+ def __init__(self, exception_class: type) -> None:
355
+ self.exception_class = exception_class
356
+ self.exception: BaseException | None = None
357
+
358
+ def __enter__(self) -> "_RaisesContext":
359
+ return self
360
+
361
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
362
+ if exc_type is None:
363
+ raise AssertionError(
364
+ f"Expected {self.exception_class.__name__} to be raised, but nothing was"
365
+ )
366
+ if not issubclass(exc_type, self.exception_class):
367
+ return False # let it propagate — wrong exception type
368
+ self.exception = exc_val
369
+ return True # swallow the expected exception
370
+
371
+
372
+ __all__ = [
373
+ # Base class
374
+ "Test",
375
+ # Assertions — positional (actual, expected, message)
376
+ "assert_equal",
377
+ "assert_not_equal",
378
+ "assert_true",
379
+ "assert_false",
380
+ "assert_none",
381
+ "assert_not_none",
382
+ "assert_in",
383
+ "assert_not_in",
384
+ "assert_is_instance",
385
+ "assert_greater",
386
+ "assert_less",
387
+ "assert_almost_equal",
388
+ "assert_raises",
389
+ ]
File without changes