codeplain 0.3.12.dev24__tar.gz → 0.3.12.dev26__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 (282) hide show
  1. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/PKG-INFO +1 -1
  2. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/docs/plain2code_cli.md +2 -0
  3. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/file_utils.py +0 -15
  4. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code.py +20 -0
  5. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_arguments.py +2 -1
  6. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_file_utils.py +2 -58
  7. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_render_finished_report.py +3 -0
  8. codeplain-0.3.12.dev26/tests/test_terminal_attachment.py +295 -0
  9. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.claude/settings.json +0 -0
  10. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.flake8 +0 -0
  11. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.github/dependabot.yml +0 -0
  12. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.github/workflows/e2e.yml +0 -0
  13. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.github/workflows/lint-and-test.yml +0 -0
  14. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.github/workflows/lint-powershell.yml +0 -0
  15. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.github/workflows/nofity-slack-on-main-merge.yml +0 -0
  16. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.github/workflows/publish-dev-to-pypi.yml +0 -0
  17. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.github/workflows/publish-install-script.yml +0 -0
  18. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.github/workflows/publish-to-pypi.yml +0 -0
  19. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.gitignore +0 -0
  20. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.vscode/launch.json +0 -0
  21. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/.vscode/settings.json +0 -0
  22. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/CLAUDE.md +0 -0
  23. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/CONTRIBUTING.md +0 -0
  24. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/LICENSE +0 -0
  25. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/README.md +0 -0
  26. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/change_detection.py +0 -0
  27. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/cli_output/__init__.py +0 -0
  28. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/cli_output/dry_run.py +0 -0
  29. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/cli_output/render_summary.py +0 -0
  30. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/cli_output/status.py +0 -0
  31. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/codeplain_REST_api.py +0 -0
  32. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/concept_utils.py +0 -0
  33. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/config/__init__.py +0 -0
  34. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/config/system_config.yaml +0 -0
  35. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/conformance_fix_journal.py +0 -0
  36. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/diff_utils.py +0 -0
  37. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/docs/generate_cli.py +0 -0
  38. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/docs/starting_a_plain_project_from_scratch.md +0 -0
  39. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/event_bus.py +0 -0
  40. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_golang/config.yaml +0 -0
  41. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_golang/harness_tests/hello_world_test.go +0 -0
  42. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_golang/hello_world_golang.plain +0 -0
  43. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_golang/run.sh +0 -0
  44. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_python/config.yaml +0 -0
  45. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py +0 -0
  46. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_python/hello_world_python.plain +0 -0
  47. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_python/run.sh +0 -0
  48. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_react/config.yaml +0 -0
  49. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/e2e/hello_world.cy.ts +0 -0
  50. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/support/e2e.ts +0 -0
  51. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress.config.ts +0 -0
  52. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_react/harness_tests/hello_world_display/package.json +0 -0
  53. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_react/harness_tests/hello_world_display/tsconfig.json +0 -0
  54. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_react/hello_world_react.plain +0 -0
  55. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/example_hello_world_react/run.sh +0 -0
  56. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/examples/run.sh +0 -0
  57. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/git_preflight.py +0 -0
  58. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/git_utils.py +0 -0
  59. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/install/bash/examples.sh +0 -0
  60. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/install/bash/install.sh +0 -0
  61. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/install/bash/walkthrough.sh +0 -0
  62. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/install/powershell/examples.ps1 +0 -0
  63. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/install/powershell/install.ps1 +0 -0
  64. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/install/powershell/walkthrough.ps1 +0 -0
  65. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/memory_management.py +0 -0
  66. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/metadata_utils.py +0 -0
  67. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/module_renderer.py +0 -0
  68. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/partial_rendering.py +0 -0
  69. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/path_resolution.py +0 -0
  70. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_cli.md +0 -0
  71. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_console.py +0 -0
  72. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_events.py +0 -0
  73. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_exceptions.py +0 -0
  74. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_logger.py +0 -0
  75. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_nodes.py +0 -0
  76. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_read_config.py +0 -0
  77. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_state.py +0 -0
  78. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_telemetry.py +0 -0
  79. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain2code_utils.py +0 -0
  80. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain_file.py +0 -0
  81. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain_modules.py +0 -0
  82. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/plain_spec.py +0 -0
  83. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/pyproject.toml +0 -0
  84. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/pytest.ini +0 -0
  85. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/__init__.py +0 -0
  86. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/analyze_specification_ambiguity.py +0 -0
  87. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/base_action.py +0 -0
  88. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/commit_conformance_tests_changes.py +0 -0
  89. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/commit_implementation_code_changes.py +0 -0
  90. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/create_dist.py +0 -0
  91. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/distill_conformance_test_memory.py +0 -0
  92. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/exit_with_error.py +0 -0
  93. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/finish_functional_requirement.py +0 -0
  94. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/fix_conformance_test.py +0 -0
  95. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/fix_unit_tests.py +0 -0
  96. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/prepare_repositories.py +0 -0
  97. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/prepare_testing_environment.py +0 -0
  98. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/refactor_code.py +0 -0
  99. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/render_conformance_tests.py +0 -0
  100. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/render_functional_requirement.py +0 -0
  101. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/run_conformance_tests.py +0 -0
  102. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/run_unit_tests.py +0 -0
  103. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/actions/summarize_conformance_tests.py +0 -0
  104. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/code_renderer.py +0 -0
  105. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/conformance_tests.py +0 -0
  106. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/implementation_code_helpers.py +0 -0
  107. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/render_context.py +0 -0
  108. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/render_types.py +0 -0
  109. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/render_utils.py +0 -0
  110. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/state_machine_config.py +0 -0
  111. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/states.py +0 -0
  112. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/render_machine/triggers.py +0 -0
  113. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/requirements.txt +0 -0
  114. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/resources/codeplain_overview.png +0 -0
  115. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/resources/plain_example.png +0 -0
  116. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/standard_template_library/golang-console-app-template.plain +0 -0
  117. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/standard_template_library/python-console-app-template.plain +0 -0
  118. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/standard_template_library/typescript-react-app-boilerplate.plain +0 -0
  119. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/standard_template_library/typescript-react-app-template.plain +0 -0
  120. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/system_config.py +0 -0
  121. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_conformance_tests_cypress.ps1 +0 -0
  122. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_conformance_tests_cypress.sh +0 -0
  123. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_conformance_tests_golang.ps1 +0 -0
  124. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_conformance_tests_golang.sh +0 -0
  125. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_conformance_tests_python.ps1 +0 -0
  126. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_conformance_tests_python.sh +0 -0
  127. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_unittests_flutter.ps1 +0 -0
  128. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_unittests_flutter.sh +0 -0
  129. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_unittests_golang.ps1 +0 -0
  130. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_unittests_golang.sh +0 -0
  131. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_unittests_python.ps1 +0 -0
  132. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_unittests_python.sh +0 -0
  133. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_unittests_react.ps1 +0 -0
  134. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/test_scripts/run_unittests_react.sh +0 -0
  135. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/__init__.py +0 -0
  136. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/conftest.py +0 -0
  137. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/acceptance_tests_warning/main_requiring_acceptance_tests.plain +0 -0
  138. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/acceptance_tests_warning/required_with_acceptance_tests.plain +0 -0
  139. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/circular_imports_1.plain +0 -0
  140. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/circular_imports_2.plain +0 -0
  141. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/circular_imports_main.plain +0 -0
  142. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/diamond_import_1.plain +0 -0
  143. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/diamond_import_2.plain +0 -0
  144. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/diamond_import_common.plain +0 -0
  145. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/diamond_imports_main.plain +0 -0
  146. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/import_with_functionalities.plain +0 -0
  147. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
  148. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/imports/non_existent_import.plain +0 -0
  149. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/partial_rendering/pr_code_var.plain +0 -0
  150. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/partial_rendering/pr_implement.plain +0 -0
  151. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/partial_rendering/pr_leaf.plain +0 -0
  152. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/partial_rendering/pr_middle.plain +0 -0
  153. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/partial_rendering/pr_root.plain +0 -0
  154. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/partial_rendering/pr_solo.plain +0 -0
  155. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  156. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  157. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
  158. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  159. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  160. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
  161. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  162. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/plain_source_with_missing_link.plain +0 -0
  163. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  164. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  165. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
  166. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  167. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  168. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  169. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  170. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  171. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  172. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  173. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  174. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  175. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  176. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  177. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  178. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
  179. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  180. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  181. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
  182. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  183. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
  184. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
  185. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
  186. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
  187. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
  188. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
  189. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
  190. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  191. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  192. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
  193. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
  194. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
  195. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  196. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  197. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  198. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  199. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  200. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  201. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts.plain +0 -0
  202. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  203. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  204. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  205. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  206. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  207. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  208. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  209. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  210. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
  211. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/topological_sort.plain +0 -0
  212. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  213. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/circular_requires_main.plain +0 -0
  214. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/circular_requires_sub.plain +0 -0
  215. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/diamond_requires_1.plain +0 -0
  216. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/diamond_requires_2.plain +0 -0
  217. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/diamond_requires_common.plain +0 -0
  218. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/diamond_requires_main.plain +0 -0
  219. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/independent_requires_1.plain +0 -0
  220. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/independent_requires_2.plain +0 -0
  221. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/independent_requires_main.plain +0 -0
  222. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/non_existent_require.plain +0 -0
  223. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/normal_requires_1.plain +0 -0
  224. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/normal_requires_2.plain +0 -0
  225. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/normal_requires_common.plain +0 -0
  226. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/requires/normal_requires_main.plain +0 -0
  227. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/sample_base64_image.txt +0 -0
  228. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/simple.plain +0 -0
  229. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/templates/block_level_include.plain +0 -0
  230. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/templates/code_variables.plain +0 -0
  231. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/templates/header.plain +0 -0
  232. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/templates/implement.plain +0 -0
  233. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/templates/implement_2.plain +0 -0
  234. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/templates/template_include.plain +0 -0
  235. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/data/templates/test_hardest_problem.plain +0 -0
  236. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/e2e/Dockerfile +0 -0
  237. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/e2e/conftest.py +0 -0
  238. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/e2e/test_hello_world_python.py +0 -0
  239. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/e2e/test_hello_world_python_windows.py +0 -0
  240. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_arg_provenance.py +0 -0
  241. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_change_detection.py +0 -0
  242. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_cli_output.py +0 -0
  243. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_conformance_fix_journal.py +0 -0
  244. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_console_log_styles.py +0 -0
  245. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_distill_conformance_test_memory.py +0 -0
  246. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_dry_run_link_validation.py +0 -0
  247. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_fix_unit_tests_conformance_context.py +0 -0
  248. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_folder_path_resolution.py +0 -0
  249. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_git_preflight.py +0 -0
  250. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_git_utils.py +0 -0
  251. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_headless_logging.py +0 -0
  252. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_imports.py +0 -0
  253. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_log_file_name_resolution.py +0 -0
  254. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_logging_config_path_resolution.py +0 -0
  255. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_partial_rendering.py +0 -0
  256. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_path_resolution.py +0 -0
  257. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_plain2code.py +0 -0
  258. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_plain2code_state.py +0 -0
  259. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_plain2code_utils.py +0 -0
  260. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_plain_modules.py +0 -0
  261. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_plainfile.py +0 -0
  262. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_plainfileparser.py +0 -0
  263. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_plainspec.py +0 -0
  264. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_prepare_repositories_layout.py +0 -0
  265. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_requires.py +0 -0
  266. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_resolve_config_file.py +0 -0
  267. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_script_path_resolution.py +0 -0
  268. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_system_config.py +0 -0
  269. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_telemetry.py +0 -0
  270. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_tui_components.py +0 -0
  271. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_tui_usage.py +0 -0
  272. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tests/test_usage_summary.py +0 -0
  273. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/__init__.py +0 -0
  274. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/components.py +0 -0
  275. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/models.py +0 -0
  276. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/plain2code_tui.py +0 -0
  277. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/plain_module_render_choice_tui.py +0 -0
  278. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/spinner.py +0 -0
  279. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/state_handlers.py +0 -0
  280. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/styles.css +0 -0
  281. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/tui/widget_helpers.py +0 -0
  282. {codeplain-0.3.12.dev24 → codeplain-0.3.12.dev26}/usage_summary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeplain
3
- Version: 0.3.12.dev24
3
+ Version: 0.3.12.dev26
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -121,6 +121,8 @@ options:
121
121
  --headless Run in headless mode: no TUI. Progress is written to
122
122
  the log file and streamed to stdout as it happens; the
123
123
  specification text itself stays in the log file only.
124
+ Used automatically when stdin, stdout or stderr is not
125
+ a terminal.
124
126
  --status Display account status including user information, API
125
127
  key label, and rendering credits. Does not render any
126
128
  code.
@@ -142,20 +142,7 @@ def get_existing_files_content(build_folder, existing_files):
142
142
  return existing_files_content
143
143
 
144
144
 
145
- def ensure_final_newline(content: str) -> str:
146
- """Ends non-empty text with a newline, as linters expect (e.g. RuboCop, ESLint eol-last, Ruff W292)."""
147
- if content and not content.endswith("\n"):
148
- return content + "\n"
149
- return content
150
-
151
-
152
145
  def store_response_files(target_folder, response_files, existing_files):
153
- """Writes the files returned by the API to target_folder. None content deletes the file.
154
-
155
- Every written file ends with a newline - whole-file rewrites come back from the API without one. The
156
- added newline is also written back into response_files, so callers that diff response_files against
157
- the folder afterwards see exactly what is on disk.
158
- """
159
146
  for file_name in response_files:
160
147
  full_file_name = os.path.join(target_folder, file_name)
161
148
 
@@ -169,8 +156,6 @@ def store_response_files(target_folder, response_files, existing_files):
169
156
 
170
157
  continue
171
158
 
172
- response_files[file_name] = ensure_final_newline(response_files[file_name])
173
-
174
159
  os.makedirs(os.path.dirname(full_file_name), exist_ok=True)
175
160
 
176
161
  with open(full_file_name, "w", encoding="utf-8") as f:
@@ -161,6 +161,17 @@ def setup_logging(
161
161
  return logging.getLevelName(configured_log_level)
162
162
 
163
163
 
164
+ def attached_to_terminal() -> bool:
165
+ """Whether a person at a terminal can see and drive the TUI.
166
+
167
+ The TUI reads keys from stdin and draws on stderr; the exit summary goes to
168
+ stdout. Started from a script or an agent, stdin is usually at end-of-file,
169
+ and Textual's input thread then reads EOF in a tight loop at 100% CPU while
170
+ a stopped render waits for an Enter nobody can press.
171
+ """
172
+ return all(stream is not None and stream.isatty() for stream in (sys.stdin, sys.stdout, sys.stderr))
173
+
174
+
164
175
  def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI) -> Optional[str]:
165
176
  """Check API connectivity, validate API key and client version, and return the user email."""
166
177
  response = codeplainAPI.connection_check(system_config.client_version)
@@ -473,12 +484,21 @@ def main(): # noqa: C901
473
484
 
474
485
  run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)
475
486
 
487
+ # Without a terminal nobody can watch or drive the TUI, so render as --headless does:
488
+ # progress on stdout, and a non-zero exit status when the render stops.
489
+ no_terminal = not args.headless and not attached_to_terminal()
490
+ if no_terminal:
491
+ args.headless = True
492
+
476
493
  if args.headless:
477
494
  # Suppress Rich console output.
478
495
  console.quiet = True
479
496
 
480
497
  default_log_level = setup_logging(args, event_bus, run_state, args.log_to_file, args.log_file_name, args.headless)
481
498
 
499
+ if no_terminal:
500
+ console.info("No terminal attached, so rendering without the TUI, as with --headless.")
501
+
482
502
  initialize_telemetry()
483
503
 
484
504
  exc_info = None
@@ -428,7 +428,8 @@ def create_parser():
428
428
  action="store_true",
429
429
  default=False,
430
430
  help="Run in headless mode: no TUI. Progress is written to the log file and streamed "
431
- "to stdout as it happens; the specification text itself stays in the log file only.",
431
+ "to stdout as it happens; the specification text itself stays in the log file only. "
432
+ "Used automatically when stdin, stdout or stderr is not a terminal.",
432
433
  )
433
434
 
434
435
  parser.add_argument(
@@ -3,8 +3,7 @@ import tempfile
3
3
 
4
4
  import pytest
5
5
 
6
- from diff_utils import get_code_diff
7
- from file_utils import get_existing_files_content, load_linked_resources, store_response_files
6
+ from file_utils import load_linked_resources, store_response_files
8
7
  from plain2code_exceptions import UnsupportedBase64Content, UnsupportedResourceType
9
8
  from plain2code_utils import MAX_BASE64_BLOB_LENGTH
10
9
 
@@ -85,64 +84,9 @@ def test_load_linked_resources_real_base64_image_raises(template_dir):
85
84
  def test_store_response_files_writes_unicode_as_utf8(template_dir):
86
85
  # Content with a non-cp1252 character (📍 U+1F4CD) must be written as UTF-8
87
86
  # regardless of the platform's default text encoding (e.g. cp1252 on Windows).
88
- content = "Location 📍 marker\n"
87
+ content = "Location 📍 marker"
89
88
  store_response_files(template_dir, {"notes.md": content}, [])
90
89
 
91
90
  file_path = os.path.join(template_dir, "notes.md")
92
91
  with open(file_path, "rb") as f:
93
92
  assert f.read().decode("utf-8") == content
94
-
95
-
96
- def _read_bytes(folder, file_name):
97
- with open(os.path.join(folder, file_name), "rb") as f:
98
- return f.read().decode("utf-8")
99
-
100
-
101
- @pytest.mark.parametrize(
102
- "content, expected",
103
- [
104
- ("class Foo\nend", "class Foo\nend\n"),
105
- ("class Foo\nend\n", "class Foo\nend\n"),
106
- ("class Foo\nend\n\n", "class Foo\nend\n\n"),
107
- ("class Foo\r\nend\r\n", "class Foo\r\nend\r\n"),
108
- ("", ""),
109
- ],
110
- )
111
- def test_store_response_files_ends_every_text_file_with_a_newline(template_dir, content, expected):
112
- # Linters gate on a final newline (RuboCop Layout/TrailingEmptyLines, ESLint eol-last, Ruff W292), so a
113
- # whole-file rewrite that comes back without one must not reach the disk that way.
114
- store_response_files(template_dir, {"app/foo.rb": content}, [])
115
-
116
- assert _read_bytes(template_dir, "app/foo.rb") == expected
117
-
118
-
119
- def test_store_response_files_hands_back_the_content_it_wrote(template_dir):
120
- # Callers diff the response files against the build folder after storing them (fix journal, conformance
121
- # fixes handed to the unit tests fixer), so the response files must match what is on disk.
122
- response_files = {"app/foo.rb": "class Foo\nend", "app/bar.rb": "class Bar\nend\n"}
123
-
124
- store_response_files(template_dir, response_files, [])
125
-
126
- assert response_files == {"app/foo.rb": "class Foo\nend\n", "app/bar.rb": "class Bar\nend\n"}
127
-
128
-
129
- def test_rewriting_a_file_without_its_final_newline_does_not_change_it(template_dir):
130
- existing_files = store_response_files(template_dir, {"app/foo.rb": "class Foo\nend\n"}, [])
131
- existing_files_content = get_existing_files_content(template_dir, existing_files)
132
-
133
- rewrite = {"app/foo.rb": "class Foo\nend"}
134
- store_response_files(template_dir, rewrite, existing_files)
135
-
136
- assert get_existing_files_content(template_dir, existing_files) == existing_files_content
137
- assert get_code_diff(rewrite, existing_files_content) == {}
138
-
139
-
140
- def test_store_response_files_still_deletes_a_file_whose_content_is_none(template_dir):
141
- existing_files = store_response_files(template_dir, {"app/foo.rb": "class Foo\nend\n"}, [])
142
-
143
- response_files = {"app/foo.rb": None}
144
- store_response_files(template_dir, response_files, existing_files)
145
-
146
- assert not os.path.exists(os.path.join(template_dir, "app/foo.rb"))
147
- assert existing_files == []
148
- assert response_files == {"app/foo.rb": None}
@@ -122,6 +122,9 @@ def run_main(render_side_effect=None, api_key="test-key"):
122
122
  render_side_effect(run_state)
123
123
 
124
124
  with (
125
+ # The args ask for the TUI. Under pytest there is no terminal, which would
126
+ # otherwise switch the render to headless.
127
+ patch("plain2code.attached_to_terminal", return_value=True),
125
128
  patch("plain2code.parse_arguments", return_value=make_args(api_key=api_key)),
126
129
  patch("plain2code.file_utils.get_template_directories", return_value=["templates"]),
127
130
  patch("plain2code.plain_modules.PlainModule", return_value=MagicMock(all_required_modules=[])),
@@ -0,0 +1,295 @@
1
+ """How a render behaves with and without a terminal attached.
2
+
3
+ The TUI needs a person at a terminal: it reads keys from stdin, draws on stderr,
4
+ and the console summary goes to stdout. Started from a script or an agent
5
+ (`codeplain module.plain > render.log 2>&1 &`), stdin is at end-of-file.
6
+ Textual's input thread then reads EOF in a tight loop - millions of reads a
7
+ second - so the process pins a CPU core for the whole render, and a stopped
8
+ render waits for an Enter nobody can press. One such render stayed alive for
9
+ an hour and a half at ~100% CPU until it was killed.
10
+
11
+ So without a terminal codeplain runs as --headless does, and a stopped render
12
+ exits non-zero. With a terminal the TUI is kept, and waiting for Enter must
13
+ stay idle.
14
+ """
15
+
16
+ import os
17
+ import struct
18
+ import subprocess
19
+ import sys
20
+ import textwrap
21
+ import threading
22
+ import time
23
+ from argparse import Namespace
24
+ from contextlib import ExitStack
25
+ from unittest.mock import MagicMock, patch
26
+
27
+ import pytest
28
+
29
+ import plain2code
30
+
31
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
32
+
33
+
34
+ class FakeStream:
35
+ def __init__(self, is_terminal: bool):
36
+ self._is_terminal = is_terminal
37
+
38
+ def isatty(self) -> bool:
39
+ return self._is_terminal
40
+
41
+
42
+ class TestAttachedToTerminal:
43
+ @pytest.fixture
44
+ def streams(self, monkeypatch):
45
+ def set_streams(stdin=True, stdout=True, stderr=True):
46
+ for name, value in (("stdin", stdin), ("stdout", stdout), ("stderr", stderr)):
47
+ monkeypatch.setattr(sys, name, value if value is None else FakeStream(value))
48
+
49
+ return set_streams
50
+
51
+ def test_all_three_streams_on_a_terminal(self, streams):
52
+ streams()
53
+
54
+ assert plain2code.attached_to_terminal() is True
55
+
56
+ @pytest.mark.parametrize("stream", ["stdin", "stdout", "stderr"])
57
+ def test_any_stream_off_the_terminal(self, streams, stream):
58
+ """stdin carries the keys, stderr the TUI, stdout the summary. Losing any
59
+ of them leaves a TUI nobody can drive or see."""
60
+ streams(**{stream: False})
61
+
62
+ assert plain2code.attached_to_terminal() is False
63
+
64
+ def test_a_closed_stream(self, streams):
65
+ """sys.stdin is None when the process has no stdin at all."""
66
+ streams(stdin=None)
67
+
68
+ assert plain2code.attached_to_terminal() is False
69
+
70
+
71
+ def make_args():
72
+ return Namespace(
73
+ version=False,
74
+ status=False,
75
+ full_plain=False,
76
+ dry_run=False,
77
+ api_key="test-key",
78
+ api="https://api.example.com",
79
+ filename="project.plain",
80
+ template_dir=None,
81
+ build_folder="build",
82
+ render_range=None,
83
+ render_from=None,
84
+ force_render=False,
85
+ headless=False,
86
+ replay_with=None,
87
+ log_to_file=False,
88
+ log_file_name="codeplain.log",
89
+ render_conformance_tests=False,
90
+ conformance_tests_script=None,
91
+ unittests_script=None,
92
+ prepare_environment_script=None,
93
+ )
94
+
95
+
96
+ def run_render(succeeded, terminal, render_state=None, render_choices=None):
97
+ """Drive the real main() and render() up to the module renderer.
98
+
99
+ The module renderer stands in for the state machine: it decides how the
100
+ render ended. Everything the decision under test does not touch - the API,
101
+ parsing, logging, telemetry - is stubbed.
102
+
103
+ Returns the mocks for the two TUIs and the module renderer, the args main()
104
+ rendered with, and the exit status it asked for (None when it returned
105
+ normally).
106
+ """
107
+ args = make_args()
108
+ module_renderer = MagicMock()
109
+
110
+ def make_module_renderer(codeplainAPI, plain_module, render_choice, render_range, args, run_state, *rest, **kw):
111
+ module_renderer.render_module.side_effect = lambda: run_state.set_render_succeeded(succeeded)
112
+ return module_renderer
113
+
114
+ progress_tui = MagicMock()
115
+ progress_tui.return_value.run.side_effect = lambda: progress_tui.call_args.kwargs["on_ready"]()
116
+ choice_tui = MagicMock()
117
+
118
+ patches = [
119
+ patch("plain2code.attached_to_terminal", return_value=terminal),
120
+ patch("plain2code.parse_arguments", return_value=args),
121
+ patch("plain2code.file_utils.get_template_directories", return_value=["templates"]),
122
+ patch("plain2code.plain_modules.PlainModule", return_value=MagicMock(all_required_modules=[])),
123
+ patch("plain2code.setup_logging", return_value="INFO"),
124
+ patch("plain2code.initialize_telemetry"),
125
+ patch("plain2code.stop_narrating"),
126
+ patch("plain2code.print_exit_summary"),
127
+ patch("plain2code.dump_crash_logs"),
128
+ patch("plain2code.capture_crash"),
129
+ patch("plain2code.codeplain_api.CodeplainAPI"),
130
+ patch("plain2code._check_connection", return_value="user@codeplain.ai"),
131
+ patch("plain2code.signal.signal"),
132
+ patch("plain2code.warn_if_acceptance_tests_without_conformance_script"),
133
+ patch("plain2code.get_plain_module_render_state", return_value=render_state),
134
+ patch("plain2code.get_render_choices", return_value=render_choices),
135
+ patch("plain2code.ModuleRenderer", side_effect=make_module_renderer),
136
+ patch("plain2code.Plain2CodeTUI", progress_tui),
137
+ patch("plain2code.PlainModuleRenderChoiceTUI", choice_tui),
138
+ patch.object(plain2code.console, "quiet", plain2code.console.quiet),
139
+ ]
140
+
141
+ exit_status = None
142
+ with ExitStack() as stack:
143
+ for each in patches:
144
+ stack.enter_context(each)
145
+ try:
146
+ plain2code.main()
147
+ except SystemExit as e:
148
+ exit_status = e.code
149
+
150
+ return Namespace(
151
+ progress_tui=progress_tui,
152
+ choice_tui=choice_tui,
153
+ module_renderer=module_renderer,
154
+ exit_status=exit_status,
155
+ args=args,
156
+ )
157
+
158
+
159
+ class TestRenderWithoutATerminal:
160
+ def test_the_tui_never_starts(self):
161
+ result = run_render(succeeded=False, terminal=False)
162
+
163
+ result.progress_tui.assert_not_called()
164
+ result.module_renderer.render_module.assert_called_once()
165
+
166
+ def test_a_stopped_render_exits_non_zero(self):
167
+ """The status --headless gives, so a script can tell it stopped."""
168
+ result = run_render(succeeded=False, terminal=False)
169
+
170
+ assert result.exit_status == 1
171
+
172
+ def test_a_completed_render_exits_cleanly(self):
173
+ result = run_render(succeeded=True, terminal=False)
174
+
175
+ assert result.exit_status is None
176
+
177
+ def test_says_why_it_runs_without_the_tui(self, caplog):
178
+ caplog.set_level("INFO", logger="codeplain")
179
+
180
+ run_render(succeeded=True, terminal=False)
181
+
182
+ assert "No terminal attached" in caplog.text
183
+
184
+ def test_never_asks_where_to_resume(self):
185
+ """The choice prompt is a TUI too, and would wait for a key just the same."""
186
+ destructive = MagicMock(is_destructive=True)
187
+ choices = {"restart": destructive, "other": MagicMock(), "quit": MagicMock()}
188
+
189
+ result = run_render(succeeded=True, terminal=False, render_state=MagicMock(), render_choices=choices)
190
+
191
+ result.choice_tui.assert_not_called()
192
+
193
+
194
+ class TestRenderWithATerminal:
195
+ def test_the_tui_still_runs(self):
196
+ result = run_render(succeeded=True, terminal=True)
197
+
198
+ result.progress_tui.assert_called_once()
199
+ assert result.args.headless is False
200
+
201
+ def test_says_nothing_about_a_missing_terminal(self, caplog):
202
+ caplog.set_level("INFO", logger="codeplain")
203
+
204
+ run_render(succeeded=True, terminal=True)
205
+
206
+ assert "No terminal attached" not in caplog.text
207
+
208
+
209
+ IDLE_SECONDS = 2.0
210
+
211
+ # Drives the real TUI to the stopped state, then measures the CPU the process
212
+ # spends over IDLE_SECONDS while it waits for Enter.
213
+ STOPPED_TUI = textwrap.dedent("""
214
+ import os, sys, threading, time
215
+
216
+ from event_bus import EventBus
217
+ from plain2code_events import RenderFailed
218
+ from plain2code_state import RunState
219
+ from tui.plain2code_tui import Plain2CodeTUI
220
+
221
+ report_path, idle_seconds = sys.argv[1], float(sys.argv[2])
222
+ event_bus = EventBus()
223
+
224
+ def stop_then_measure():
225
+ event_bus.publish(RenderFailed(error_message="unit tests still failing"))
226
+ start = time.process_time()
227
+ time.sleep(idle_seconds)
228
+ with open(report_path + ".tmp", "w") as report:
229
+ report.write(str(time.process_time() - start))
230
+ os.replace(report_path + ".tmp", report_path)
231
+
232
+ Plain2CodeTUI(
233
+ event_bus=event_bus,
234
+ run_state=RunState(spec_filename="x.plain"),
235
+ on_ready=threading.Thread(target=stop_then_measure, daemon=True).start,
236
+ render_id="test",
237
+ unittests_script=None,
238
+ conformance_tests_script=None,
239
+ prepare_environment_script=None,
240
+ state_machine_version="0.0.0",
241
+ css_path="styles.css",
242
+ ).run()
243
+ """)
244
+
245
+
246
+ @pytest.mark.skipif(sys.platform == "win32", reason="needs a POSIX pseudo-terminal")
247
+ def test_a_stopped_render_on_a_terminal_waits_for_enter_without_spinning(tmp_path):
248
+ # Unix-only modules: imported here so Windows can still collect the other tests in this file.
249
+ import fcntl
250
+ import termios
251
+
252
+ controller, terminal = os.openpty()
253
+ fcntl.ioctl(terminal, termios.TIOCSWINSZ, struct.pack("HHHH", 30, 100, 0, 0))
254
+ report = tmp_path / "cpu_seconds"
255
+
256
+ process = subprocess.Popen(
257
+ [sys.executable, "-c", STOPPED_TUI, str(report), str(IDLE_SECONDS)],
258
+ stdin=terminal,
259
+ stdout=terminal,
260
+ stderr=terminal,
261
+ cwd=REPO_ROOT,
262
+ env={**os.environ, "TERM": "xterm-256color"},
263
+ start_new_session=True,
264
+ )
265
+ os.close(terminal)
266
+
267
+ def drain():
268
+ # The TUI blocks once the terminal buffer fills, so keep reading it.
269
+ try:
270
+ while os.read(controller, 65536):
271
+ pass
272
+ except OSError:
273
+ pass
274
+
275
+ threading.Thread(target=drain, daemon=True).start()
276
+
277
+ try:
278
+ deadline = time.monotonic() + 60
279
+ while not report.exists():
280
+ assert process.poll() is None, "the TUI exited before Enter was pressed"
281
+ assert time.monotonic() < deadline, "the TUI never reached the stopped state"
282
+ time.sleep(0.1)
283
+ cpu_seconds = float(report.read_text())
284
+
285
+ os.write(controller, b"\r")
286
+ exit_status = process.wait(timeout=15)
287
+ finally:
288
+ if process.poll() is None:
289
+ process.kill()
290
+ process.wait()
291
+ os.close(controller)
292
+
293
+ # A busy loop burns about one CPU-second per second of waiting.
294
+ assert cpu_seconds < IDLE_SECONDS / 4
295
+ assert exit_status == 0