stateous 0.2.1__tar.gz → 0.2.2__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 (179) hide show
  1. {stateous-0.2.1 → stateous-0.2.2}/PKG-INFO +7 -1
  2. {stateous-0.2.1 → stateous-0.2.2}/README.md +6 -0
  3. {stateous-0.2.1 → stateous-0.2.2}/docs/reference/files.md +1 -1
  4. {stateous-0.2.1 → stateous-0.2.2}/pyproject.toml +1 -1
  5. {stateous-0.2.1 → stateous-0.2.2}/scripts/check.py +3 -3
  6. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/cli/main.py +6 -3
  7. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_closed_output.py +28 -0
  8. {stateous-0.2.1 → stateous-0.2.2}/.gitignore +0 -0
  9. {stateous-0.2.1 → stateous-0.2.2}/LICENSE +0 -0
  10. {stateous-0.2.1 → stateous-0.2.2}/NOTICE +0 -0
  11. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/agents.md +0 -0
  12. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/context-notes-memory.md +0 -0
  13. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/guards.md +0 -0
  14. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/model.md +0 -0
  15. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/parallel.md +0 -0
  16. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/pause-resume-cancel.md +0 -0
  17. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/reliability.md +0 -0
  18. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/tasks-and-routines.md +0 -0
  19. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/timers.md +0 -0
  20. {stateous-0.2.1 → stateous-0.2.2}/docs/concepts/two-sides.md +0 -0
  21. {stateous-0.2.1 → stateous-0.2.2}/docs/getting-started.md +0 -0
  22. {stateous-0.2.1 → stateous-0.2.2}/docs/how-to/change-a-workflow.md +0 -0
  23. {stateous-0.2.1 → stateous-0.2.2}/docs/how-to/run-supervisors.md +0 -0
  24. {stateous-0.2.1 → stateous-0.2.2}/docs/how-to/steer-tokens.md +0 -0
  25. {stateous-0.2.1 → stateous-0.2.2}/docs/how-to/use-postgres.md +0 -0
  26. {stateous-0.2.1 → stateous-0.2.2}/docs/how-to/write-a-routine.md +0 -0
  27. {stateous-0.2.1 → stateous-0.2.2}/docs/how-to/write-a-workflow.md +0 -0
  28. {stateous-0.2.1 → stateous-0.2.2}/docs/index.md +0 -0
  29. {stateous-0.2.1 → stateous-0.2.2}/docs/inside/data.md +0 -0
  30. {stateous-0.2.1 → stateous-0.2.2}/docs/inside/event-graph.md +0 -0
  31. {stateous-0.2.1 → stateous-0.2.2}/docs/inside/workflows.md +0 -0
  32. {stateous-0.2.1 → stateous-0.2.2}/docs/reference/cli.md +0 -0
  33. {stateous-0.2.1 → stateous-0.2.2}/docs/reference/http.md +0 -0
  34. {stateous-0.2.1 → stateous-0.2.2}/docs/reference/limits.md +0 -0
  35. {stateous-0.2.1 → stateous-0.2.2}/docs/reference/python.md +0 -0
  36. {stateous-0.2.1 → stateous-0.2.2}/examples/sdlc/build/sdlc.json +0 -0
  37. {stateous-0.2.1 → stateous-0.2.2}/examples/sdlc/flow.py +0 -0
  38. {stateous-0.2.1 → stateous-0.2.2}/examples/sdlc/schemas.py +0 -0
  39. {stateous-0.2.1 → stateous-0.2.2}/examples/sdlc-supervisor/coding.py +0 -0
  40. {stateous-0.2.1 → stateous-0.2.2}/examples/sdlc-supervisor/contract.py +0 -0
  41. {stateous-0.2.1 → stateous-0.2.2}/examples/sdlc-supervisor/stateous.yaml +0 -0
  42. {stateous-0.2.1 → stateous-0.2.2}/examples/sdlc-supervisor/testing.py +0 -0
  43. {stateous-0.2.1 → stateous-0.2.2}/mkdocs.yml +0 -0
  44. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/__init__.py +0 -0
  45. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/__main__.py +0 -0
  46. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/cli/__init__.py +0 -0
  47. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/cli/errors.py +0 -0
  48. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/cli/project_file.py +0 -0
  49. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/cli/running.py +0 -0
  50. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/cli/serving.py +0 -0
  51. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/cli/starter.py +0 -0
  52. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/cli/tokens.py +0 -0
  53. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/client/__init__.py +0 -0
  54. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/client/client.py +0 -0
  55. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/compiler/__init__.py +0 -0
  56. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/compiler/codegen.py +0 -0
  57. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/compiler/compile.py +0 -0
  58. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/compiler/exclusive.py +0 -0
  59. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/compiler/load.py +0 -0
  60. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/compiler/plot.py +0 -0
  61. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/compiler/validate.py +0 -0
  62. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/__init__.py +0 -0
  63. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/api.py +0 -0
  64. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/engine.py +0 -0
  65. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/graph.py +0 -0
  66. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/__init__.py +0 -0
  67. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/errors.py +0 -0
  68. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/migrations/__init__.py +0 -0
  69. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/migrations/env.py +0 -0
  70. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/migrations/script.py.mako +0 -0
  71. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/migrations/versions/0001_baseline.py +0 -0
  72. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/migrations/versions/0002_claims.py +0 -0
  73. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/migrations/versions/0003_children.py +0 -0
  74. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/store.py +0 -0
  75. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/control/store/tables.py +0 -0
  76. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/__init__.py +0 -0
  77. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/context.py +0 -0
  78. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/contract.py +0 -0
  79. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/durations.py +0 -0
  80. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/events.py +0 -0
  81. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/graph_file.py +0 -0
  82. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/guards.py +0 -0
  83. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/host.py +0 -0
  84. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/wire.py +0 -0
  85. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/model/workflow.py +0 -0
  86. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/py.typed +0 -0
  87. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/supervisor/__init__.py +0 -0
  88. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/supervisor/folder.py +0 -0
  89. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/supervisor/outbox.py +0 -0
  90. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/supervisor/registry.py +0 -0
  91. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/supervisor/remote.py +0 -0
  92. {stateous-0.2.1 → stateous-0.2.2}/src/stateous/supervisor/supervisor.py +0 -0
  93. {stateous-0.2.1 → stateous-0.2.2}/tests/conftest.py +0 -0
  94. {stateous-0.2.1 → stateous-0.2.2}/tests/docs/pages.py +0 -0
  95. {stateous-0.2.1 → stateous-0.2.2}/tests/docs/test_concepts.py +0 -0
  96. {stateous-0.2.1 → stateous-0.2.2}/tests/docs/test_how_to.py +0 -0
  97. {stateous-0.2.1 → stateous-0.2.2}/tests/docs/test_inside.py +0 -0
  98. {stateous-0.2.1 → stateous-0.2.2}/tests/docs/test_numbers.py +0 -0
  99. {stateous-0.2.1 → stateous-0.2.2}/tests/docs/test_reference.py +0 -0
  100. {stateous-0.2.1 → stateous-0.2.2}/tests/docs/test_site.py +0 -0
  101. {stateous-0.2.1 → stateous-0.2.2}/tests/e2e/test_example.py +0 -0
  102. {stateous-0.2.1 → stateous-0.2.2}/tests/examples/fake_phd.py +0 -0
  103. {stateous-0.2.1 → stateous-0.2.2}/tests/examples/test_coding.py +0 -0
  104. {stateous-0.2.1 → stateous-0.2.2}/tests/examples/test_sdlc.py +0 -0
  105. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/conftest.py +0 -0
  106. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/supervising.py +0 -0
  107. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_api_parallel.py +0 -0
  108. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_api_refusals.py +0 -0
  109. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_api_tasks.py +0 -0
  110. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_api_tokens.py +0 -0
  111. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_client.py +0 -0
  112. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_parallel_flow.py +0 -0
  113. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_prepare_serve.py +0 -0
  114. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_project_uv_sync.py +0 -0
  115. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_serve_command.py +0 -0
  116. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_supervise_cli.py +0 -0
  117. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_supervise_process.py +0 -0
  118. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_supervisor_flow.py +0 -0
  119. {stateous-0.2.1 → stateous-0.2.2}/tests/integration/test_token_commands.py +0 -0
  120. {stateous-0.2.1 → stateous-0.2.2}/tests/parallel.py +0 -0
  121. {stateous-0.2.1 → stateous-0.2.2}/tests/postgres/test_concurrency.py +0 -0
  122. {stateous-0.2.1 → stateous-0.2.2}/tests/postgres/test_connection.py +0 -0
  123. {stateous-0.2.1 → stateous-0.2.2}/tests/postgres/test_migrations.py +0 -0
  124. {stateous-0.2.1 → stateous-0.2.2}/tests/postgres/test_serving.py +0 -0
  125. {stateous-0.2.1 → stateous-0.2.2}/tests/postgres/test_time_limits.py +0 -0
  126. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_clock.py +0 -0
  127. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_compile_command.py +0 -0
  128. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_configure_logging.py +0 -0
  129. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_errors.py +0 -0
  130. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_graceful_server.py +0 -0
  131. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_init_workflow.py +0 -0
  132. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_listen.py +0 -0
  133. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_project_file.py +0 -0
  134. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_show_children.py +0 -0
  135. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_startup.py +0 -0
  136. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_supervise_helpers.py +0 -0
  137. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_supervised_server.py +0 -0
  138. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/cli/test_token_lines.py +0 -0
  139. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/compiler/test_codegen.py +0 -0
  140. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/compiler/test_compile.py +0 -0
  141. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/compiler/test_exclusive.py +0 -0
  142. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/compiler/test_load.py +0 -0
  143. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/compiler/test_plot.py +0 -0
  144. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/compiler/test_validate.py +0 -0
  145. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/support.py +0 -0
  146. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_case_race.py +0 -0
  147. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_claim_requests.py +0 -0
  148. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_clock.py +0 -0
  149. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_deadlock.py +0 -0
  150. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_engine.py +0 -0
  151. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_graph.py +0 -0
  152. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_inputs.py +0 -0
  153. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_migrations.py +0 -0
  154. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_parallel.py +0 -0
  155. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_parallel_steering.py +0 -0
  156. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_pause.py +0 -0
  157. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_serving.py +0 -0
  158. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_store.py +0 -0
  159. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/control/test_tasks.py +0 -0
  160. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/model/test_context.py +0 -0
  161. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/model/test_contract.py +0 -0
  162. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/model/test_durations.py +0 -0
  163. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/model/test_graph_file.py +0 -0
  164. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/model/test_guards.py +0 -0
  165. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/model/test_host.py +0 -0
  166. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/model/test_wire.py +0 -0
  167. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/model/test_workflow.py +0 -0
  168. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/fakes.py +0 -0
  169. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/test_folder.py +0 -0
  170. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/test_open.py +0 -0
  171. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/test_outbox.py +0 -0
  172. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/test_registry.py +0 -0
  173. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/test_remote.py +0 -0
  174. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/test_supervisor.py +0 -0
  175. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/test_supervisor_edges.py +0 -0
  176. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/supervisor/test_supervisor_outbox.py +0 -0
  177. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/test_layout.py +0 -0
  178. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/test_public_names.py +0 -0
  179. {stateous-0.2.1 → stateous-0.2.2}/tests/unit/test_test_databases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: stateous
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A process as a graph of states. A control plane keeps every token durable.
5
5
  Project-URL: Homepage, https://github.com/StateousLabs/stateous
6
6
  Project-URL: Source, https://github.com/StateousLabs/stateous
@@ -38,6 +38,12 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  # Stateous
40
40
 
41
+ [![CI](https://github.com/StateousLabs/stateous/actions/workflows/ci.yml/badge.svg)](https://github.com/StateousLabs/stateous/actions/workflows/ci.yml)
42
+ [![PyPI](https://img.shields.io/pypi/v/stateous?cacheSeconds=3600)](https://pypi.org/project/stateous/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/stateous?cacheSeconds=3600)](https://pypi.org/project/stateous/)
44
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://stateouslabs.github.io/stateous/)
45
+ [![License](https://img.shields.io/pypi/l/stateous?cacheSeconds=3600)](https://github.com/StateousLabs/stateous/blob/main/LICENSE)
46
+
41
47
  Stateous runs a process as a graph of states, and keeps every token of it durable.
42
48
 
43
49
  You write the graph in Python. A control plane keeps every token and chooses each next state. A supervisor, on any computer, does the work and reports what happened. The supervisor never sees the graph. It knows only which states it serves, and which outcomes each state allows.
@@ -1,5 +1,11 @@
1
1
  # Stateous
2
2
 
3
+ [![CI](https://github.com/StateousLabs/stateous/actions/workflows/ci.yml/badge.svg)](https://github.com/StateousLabs/stateous/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/stateous?cacheSeconds=3600)](https://pypi.org/project/stateous/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/stateous?cacheSeconds=3600)](https://pypi.org/project/stateous/)
6
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://stateouslabs.github.io/stateous/)
7
+ [![License](https://img.shields.io/pypi/l/stateous?cacheSeconds=3600)](https://github.com/StateousLabs/stateous/blob/main/LICENSE)
8
+
3
9
  Stateous runs a process as a graph of states, and keeps every token of it durable.
4
10
 
5
11
  You write the graph in Python. A control plane keeps every token and chooses each next state. A supervisor, on any computer, does the work and reports what happened. The supervisor never sees the graph. It knows only which states it serves, and which outcomes each state allows.
@@ -149,7 +149,7 @@ With `--stateous-path PATH`, the file also has a `[tool.uv.sources]` table. The
149
149
  name = "sdlc-supervisor"
150
150
  version = "0.1.0"
151
151
  requires-python = ">=3.12"
152
- dependencies = ["stateous>=0.2.1"]
152
+ dependencies = ["stateous>=0.2.2"]
153
153
 
154
154
  # A local checkout of Stateous: delete this table to use Stateous from PyPI.
155
155
  [tool.uv.sources]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stateous"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "A process as a graph of states. A control plane keeps every token durable."
5
5
  license = "Apache-2.0"
6
6
  license-files = ["LICENSE", "NOTICE"]
@@ -1,8 +1,8 @@
1
1
  """Every check, in order: `uv run python scripts/check.py`.
2
2
 
3
- GitHub Actions does not run for this repository, so this script is the gate
4
- before a commit. It runs the lint rules, the format check, the types, the import
5
- contracts, the build of the documentation site and the tests. It stops at the first
3
+ Run it before a commit. CI (.github/workflows/ci.yml) runs it too. It runs the lint
4
+ rules, the format check, the types, the import contracts, the build of the
5
+ documentation site and the tests. It stops at the first
6
6
  check that fails, and names it.
7
7
  """
8
8
 
@@ -294,12 +294,15 @@ def main() -> None:
294
294
  except OSError as error:
295
295
  if not _closed_pipe(error, stdout.failure):
296
296
  raise
297
- # Python flushes stdout again at exit: send it to nothing, so that flush cannot fail.
298
- nothing = os.open(os.devnull, os.O_WRONLY)
299
- os.dup2(nothing, sys.stdout.fileno())
300
297
  sys.exit(1)
301
298
  finally:
302
299
  sys.stdout = stdout.stream
300
+ # Python flushes stdout again at exit: send it to nothing, so that flush cannot
301
+ # fail. Also when Click caught the EPIPE itself and exited with code 1.
302
+ if stdout.failure is not None and _closed_pipe(stdout.failure, stdout.failure):
303
+ nothing = os.open(os.devnull, os.O_WRONLY)
304
+ os.dup2(nothing, sys.stdout.fileno())
305
+ os.close(nothing)
303
306
 
304
307
 
305
308
  class _Stdout:
@@ -107,3 +107,31 @@ def test_einval_from_a_write_to_stdout_is_quiet(
107
107
 
108
108
  assert raised.value.code == 1
109
109
  assert capsys.readouterr().err == ""
110
+
111
+
112
+ class BrokenStdout(ClosedStdout):
113
+ """Stdout whose reader closed the pipe, as Linux and macOS report it: EPIPE. Click
114
+ catches EPIPE itself and exits with code 1."""
115
+
116
+ def write(self, text: str) -> int:
117
+ raise BrokenPipeError(errno.EPIPE, "Broken pipe")
118
+
119
+
120
+ def test_epipe_sends_the_rest_of_stdout_to_nothing(
121
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
122
+ ) -> None:
123
+ """Else Python flushes stdout again at exit, that flush fails, and the exit code is 120."""
124
+ target = tmp_path / "stdout"
125
+ fd = os.open(target, os.O_WRONLY | os.O_CREAT)
126
+ try:
127
+ monkeypatch.setattr(sys, "stdout", BrokenStdout(fd))
128
+
129
+ with pytest.raises(SystemExit) as raised:
130
+ run_main("compile", str(FLOW), "--out", str(tmp_path / "sdlc.json"))
131
+
132
+ os.write(fd, b"after the command")
133
+ finally:
134
+ os.close(fd)
135
+
136
+ assert raised.value.code == 1
137
+ assert target.read_bytes() == b""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes