openprocess 0.7.0__tar.gz → 0.7.1__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 (204) hide show
  1. {openprocess-0.7.0/openprocess.egg-info → openprocess-0.7.1}/PKG-INFO +1 -1
  2. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/__init__.py +1 -1
  3. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/box.py +8 -3
  4. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/app.py +77 -6
  5. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/updates.py +143 -18
  6. {openprocess-0.7.0 → openprocess-0.7.1/openprocess.egg-info}/PKG-INFO +1 -1
  7. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_updates.py +102 -1
  8. {openprocess-0.7.0 → openprocess-0.7.1}/LICENSE +0 -0
  9. {openprocess-0.7.0 → openprocess-0.7.1}/README.md +0 -0
  10. {openprocess-0.7.0 → openprocess-0.7.1}/cpnpy/__init__.py +0 -0
  11. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/analysis/__init__.py +0 -0
  12. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/analysis/state_space.py +0 -0
  13. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/analysis/state_space_process.py +0 -0
  14. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/cli.py +0 -0
  15. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
  16. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
  17. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
  18. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
  19. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
  20. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
  21. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
  22. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
  23. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
  24. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
  25. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
  26. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
  27. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
  28. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
  29. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
  30. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
  31. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
  32. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
  33. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
  34. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/pack.md +0 -0
  35. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/__init__.py +0 -0
  36. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/__init__.py +0 -0
  37. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/check.py +0 -0
  38. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/compare.py +0 -0
  39. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/cpn.py +0 -0
  40. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/discover.py +0 -0
  41. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/filter.py +0 -0
  42. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/input.py +0 -0
  43. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/output.py +0 -0
  44. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/predict.py +0 -0
  45. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/science.py +0 -0
  46. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/sweeps.py +0 -0
  47. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/convert.py +0 -0
  48. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/datasets.py +0 -0
  49. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/explain.py +0 -0
  50. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/library.py +0 -0
  51. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/record.py +0 -0
  52. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/runner.py +0 -0
  53. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/sweep.py +0 -0
  54. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/types.py +0 -0
  55. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/workflow.py +0 -0
  56. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/__init__.py +0 -0
  57. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/app.py +0 -0
  58. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/arc_editing.py +0 -0
  59. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/canvas.py +0 -0
  60. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/__init__.py +0 -0
  61. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/canvas.py +0 -0
  62. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/page.py +0 -0
  63. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/templates.py +0 -0
  64. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/viewers.py +0 -0
  65. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/items.py +0 -0
  66. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/learn/answer_boxes.py +0 -0
  67. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/learn/concealment.py +0 -0
  68. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/learn/mode.py +0 -0
  69. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/panning.py +0 -0
  70. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/resources/openprocess-icon.png +0 -0
  71. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/__init__.py +0 -0
  72. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/__main__.py +0 -0
  73. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/charts.py +0 -0
  74. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/compare_page.py +0 -0
  75. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/cpn_page.py +0 -0
  76. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/definition_view.py +0 -0
  77. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/derivation_view.py +0 -0
  78. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/documents.py +0 -0
  79. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/dotted_chart.py +0 -0
  80. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/file_dialogs.py +0 -0
  81. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/filter_dialog.py +0 -0
  82. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/graph_builders.py +0 -0
  83. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/graph_view.py +0 -0
  84. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/instances.py +0 -0
  85. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/log_editor.py +0 -0
  86. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/log_page.py +0 -0
  87. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/markdown_view.py +0 -0
  88. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/mathtext.py +0 -0
  89. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/ml_highlighter.py +0 -0
  90. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/model_page.py +0 -0
  91. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/net_comparison.py +0 -0
  92. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/notes_overlay.py +0 -0
  93. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/petri_page.py +0 -0
  94. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/regions_view.py +0 -0
  95. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/sidebar.py +0 -0
  96. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/style.py +0 -0
  97. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/tool_icons.py +0 -0
  98. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/widgets.py +0 -0
  99. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/workers.py +0 -0
  100. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/workspace.py +0 -0
  101. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/theme.py +0 -0
  102. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/tidy.py +0 -0
  103. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/io/__init__.py +0 -0
  104. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/io/cpn_reader.py +0 -0
  105. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/io/cpn_writer.py +0 -0
  106. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/__init__.py +0 -0
  107. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/answers.py +0 -0
  108. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/checks.py +0 -0
  109. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/computed.py +0 -0
  110. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/context.py +0 -0
  111. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/exam.py +0 -0
  112. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/exercise-packs.md +0 -0
  113. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/importer.py +0 -0
  114. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/notation.py +0 -0
  115. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/pack.py +0 -0
  116. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/sheet.py +0 -0
  117. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/__init__.py +0 -0
  118. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/analysis.py +0 -0
  119. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/columns.py +0 -0
  120. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/compare_nets.py +0 -0
  121. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/conformance/__init__.py +0 -0
  122. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/conformance/alignments.py +0 -0
  123. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/conformance/quality.py +0 -0
  124. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/conformance/token_replay.py +0 -0
  125. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/csv_import.py +0 -0
  126. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/definitions.py +0 -0
  127. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/dfg.py +0 -0
  128. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/__init__.py +0 -0
  129. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/alpha.py +0 -0
  130. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/heuristics.py +0 -0
  131. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/inductive.py +0 -0
  132. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/state_regions.py +0 -0
  133. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/filtering.py +0 -0
  134. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/footprint.py +0 -0
  135. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/invariants.py +0 -0
  136. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/layout.py +0 -0
  137. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/log.py +0 -0
  138. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/petrinet.py +0 -0
  139. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/playout.py +0 -0
  140. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/pm4py_bridge.py +0 -0
  141. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/pnml.py +0 -0
  142. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/processtree.py +0 -0
  143. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/regions.py +0 -0
  144. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/stats.py +0 -0
  145. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/structure.py +0 -0
  146. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/transition_system.py +0 -0
  147. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/xes.py +0 -0
  148. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/__init__.py +0 -0
  149. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/ast_nodes.py +0 -0
  150. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/builtins.py +0 -0
  151. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/colorsets.py +0 -0
  152. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/errors.py +0 -0
  153. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/evaluator.py +0 -0
  154. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/lexer.py +0 -0
  155. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/multiset.py +0 -0
  156. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/parser.py +0 -0
  157. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/values.py +0 -0
  158. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/__init__.py +0 -0
  159. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/declarations.py +0 -0
  160. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/examples.py +0 -0
  161. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/net.py +0 -0
  162. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/plain.py +0 -0
  163. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/references.py +0 -0
  164. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/sim/__init__.py +0 -0
  165. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/sim/binding.py +0 -0
  166. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/sim/export.py +0 -0
  167. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/sim/simulator.py +0 -0
  168. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/__init__.py +0 -0
  169. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/answers.py +0 -0
  170. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/checks.py +0 -0
  171. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/pack.py +0 -0
  172. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/sheet.py +0 -0
  173. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/SOURCES.txt +0 -0
  174. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/dependency_links.txt +0 -0
  175. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/entry_points.txt +0 -0
  176. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/requires.txt +0 -0
  177. {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/top_level.txt +0 -0
  178. {openprocess-0.7.0 → openprocess-0.7.1}/pyproject.toml +0 -0
  179. {openprocess-0.7.0 → openprocess-0.7.1}/setup.cfg +0 -0
  180. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_arc_editing.py +0 -0
  181. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_colorsets.py +0 -0
  182. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_columns.py +0 -0
  183. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_cpn_course_model.py +0 -0
  184. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_definitions.py +0 -0
  185. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_exercises_gui.py +0 -0
  186. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_flow.py +0 -0
  187. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_flow_gui.py +0 -0
  188. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_hierarchy.py +0 -0
  189. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_io.py +0 -0
  190. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_learn.py +0 -0
  191. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_learn_gui.py +0 -0
  192. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_log_editing.py +0 -0
  193. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_mining.py +0 -0
  194. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_ml.py +0 -0
  195. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_multiset.py +0 -0
  196. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_regions.py +0 -0
  197. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_rename.py +0 -0
  198. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_simulation.py +0 -0
  199. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_state_space.py +0 -0
  200. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_studio_gui.py +0 -0
  201. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_teaching.py +0 -0
  202. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_tidy.py +0 -0
  203. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_wf_structure.py +0 -0
  204. {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Process mining, Petri nets and workflows in one open app: every algorithm visible, every result reproducible, every step teachable
5
5
  Author: Sam Struthers
6
6
  License: MIT
@@ -48,7 +48,7 @@ from .sim.simulator import Simulator
48
48
  #: The one place the version number is written (pyproject.toml, the update
49
49
  #: check and packaging/build.py read it from here). A release is tagged
50
50
  #: "v" + this, e.g. v0.2.0.
51
- __version__ = "0.7.0"
51
+ __version__ = "0.7.1"
52
52
 
53
53
  __all__ = [
54
54
  "Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
@@ -374,10 +374,15 @@ class Called:
374
374
 
375
375
  @property
376
376
  def where(self) -> str:
377
- """The file as shown: ``openprocess/mining/discovery/alpha.py:86``."""
377
+ """The file as shown: ``openprocess/mining/discovery/alpha.py:86`` for the
378
+ package's own code (whatever the folder it was cloned into is called),
379
+ else the file's name."""
378
380
  path = Path(self.file)
379
- parts = path.parts
380
- shown = "/".join(parts[parts.index("openprocess"):]) if "openprocess" in parts else path.name
381
+ root = Path(__file__).resolve().parents[2] # the folder holding the package
382
+ try:
383
+ shown = path.resolve().relative_to(root).as_posix()
384
+ except ValueError:
385
+ shown = path.name
381
386
  return shown + (f":{self.line}" if self.line else "")
382
387
 
383
388
 
@@ -150,6 +150,28 @@ def reveal_label() -> str:
150
150
 
151
151
 
152
152
 
153
+ def report_unexpected_errors() -> None:
154
+ """An error nobody caught shows a dialog with the traceback instead of
155
+ vanishing into a terminal the app may not have (in the bundle, it has none)."""
156
+ import sys
157
+ import traceback
158
+ previous = sys.excepthook
159
+
160
+ def hook(kind, value, trace) -> None:
161
+ previous(kind, value, trace)
162
+ text = "".join(traceback.format_exception(kind, value, trace))
163
+ try:
164
+ box = QMessageBox(QMessageBox.Critical, "Something went wrong",
165
+ f"{kind.__name__}: {value}")
166
+ box.setInformativeText("OpenProcess keeps running. If it happens again, please report "
167
+ "it with the details below at https://github.com/s4mstruthers/openprocess/issues.")
168
+ box.setDetailedText(text)
169
+ box.exec()
170
+ except Exception: # noqa: BLE001 - never fail inside the error reporter
171
+ pass
172
+ sys.excepthook = hook
173
+
174
+
153
175
  def app_icon() -> QIcon:
154
176
  """The OpenProcess logo (Dock, window and About box)."""
155
177
  return QIcon(str(Path(__file__).resolve().parents[1] / "resources" / "openprocess-icon.png"))
@@ -2415,16 +2437,23 @@ class StudioWindow(QMainWindow):
2415
2437
  self._show_update_dialog(release) # asked for: the dialog with the notes
2416
2438
  else:
2417
2439
  # At launch: a bar at the top that does not stop anyone working.
2418
- self.update_bar.offer(release, self._can_install(release))
2440
+ self.update_bar.offer(release, self._can_install(release), self._update_way(release)[1])
2419
2441
 
2420
- def _can_install(self, release) -> bool:
2442
+ def _update_way(self, release):
2443
+ """How this copy was installed, and how ``release`` can be put over it
2444
+ (see :func:`updates.update_way`; None: only the release page helps)."""
2421
2445
  from . import updates
2422
- return updates.installed_app() is not None and release.download_for() is not None
2446
+ install = updates.how_installed()
2447
+ return install, updates.update_way(install, release)
2448
+
2449
+ def _can_install(self, release) -> bool:
2450
+ return self._update_way(release)[1] is not None
2423
2451
 
2424
2452
  def _show_update_dialog(self, release) -> None:
2425
2453
  """What's new in ``release`` (and the versions before it), and what to do."""
2426
2454
  from . import updates
2427
- dialog = updates.UpdateDialog(release, self._can_install(release), self)
2455
+ install, way = self._update_way(release)
2456
+ dialog = updates.UpdateDialog(release, self._can_install(release), self, install, way)
2428
2457
  dialog.exec()
2429
2458
  self._update_action(release, {dialog.SKIP: "skip", dialog.PAGE: "page",
2430
2459
  dialog.INSTALL: "install"}.get(dialog.outcome))
@@ -2439,10 +2468,51 @@ class StudioWindow(QMainWindow):
2439
2468
  "about the next version", 6000)
2440
2469
  elif action == "page":
2441
2470
  QDesktopServices.openUrl(QUrl(release.page))
2442
- elif not self._can_install(release): # "How to Update" (run from source)
2471
+ elif not self._can_install(release): # "How to Update": the dialog says how
2443
2472
  self._show_update_dialog(release)
2444
2473
  else:
2445
- self._install_update(release)
2474
+ install, way = self._update_way(release)
2475
+ if way == "app":
2476
+ self._install_update(release)
2477
+ else:
2478
+ self._run_update(release, install, way)
2479
+
2480
+ def _run_update(self, release, install, way: str) -> None:
2481
+ """A pip install or a checkout: run the update commands in the background,
2482
+ then offer to restart into the new version."""
2483
+ from PySide6.QtWidgets import QProgressDialog
2484
+ from . import updates
2485
+ progress = QProgressDialog(f"Updating to OpenProcess {release.version}…", None, 0, 0, self)
2486
+ progress.setWindowTitle("Software Update")
2487
+ progress.setWindowModality(Qt.WindowModal)
2488
+ progress.setMinimumDuration(0)
2489
+ progress.setAutoClose(False)
2490
+ progress.show()
2491
+
2492
+ def failed(message: str) -> None:
2493
+ progress.close()
2494
+ QMessageBox.warning(self, "Software Update", "The update did not go through.\n\n"
2495
+ f"{message}\n\nYou can also get it from {release.page}")
2496
+
2497
+ def done(output: str) -> None:
2498
+ progress.close()
2499
+ self._after_update(release, output)
2500
+
2501
+ run_in_background(lambda: updates.run_update(install, release, way), done, failed)
2502
+
2503
+ def _after_update(self, release, output: str) -> None:
2504
+ from . import updates
2505
+ box = QMessageBox(self)
2506
+ box.setWindowTitle("Software Update")
2507
+ box.setText(f"OpenProcess {release.version} is installed.")
2508
+ box.setInformativeText("OpenProcess starts again to use it. Your open files come back.")
2509
+ box.setDetailedText(output)
2510
+ restart = box.addButton("Restart Now", QMessageBox.AcceptRole)
2511
+ box.addButton("Later", QMessageBox.RejectRole)
2512
+ box.exec()
2513
+ if box.clickedButton() is restart and self.close():
2514
+ updates.run_detached(updates.restart_command())
2515
+ QApplication.quit()
2446
2516
 
2447
2517
  def _install_update(self, release) -> None:
2448
2518
  """Download and unpack the update in the background, then restart into it."""
@@ -4005,6 +4075,7 @@ def main(argv: list[str] | None = None) -> int:
4005
4075
  application = QApplication.instance() or QApplication(argv)
4006
4076
  application.setApplicationName(APPLICATION_NAME)
4007
4077
  application.setWindowIcon(app_icon())
4078
+ report_unexpected_errors()
4008
4079
  application.setStyle("Fusion")
4009
4080
  application.setFont(theme.ui_font(13))
4010
4081
  application.setStyleSheet(style.stylesheet())
@@ -9,12 +9,20 @@ the download for this system is chosen by the names the build gives them
9
9
  OpenProcess-<version>-Windows-<arch>.zip
10
10
  OpenProcess-<version>-Linux-<arch>.tar.gz
11
11
 
12
- Installing replaces the app the user is running. The app cannot overwrite
13
- itself while it runs, so it downloads and unpacks the new version, writes a
14
- small script that waits for the app to quit, swaps the old app for the new
15
- one (keeping the old one until the new one is in place) and starts it, and
16
- then quits. OpenProcess run from source (``git clone`` + ``pip install -e``) is
17
- never touched: it only gets told what to run.
12
+ Installing depends on how this copy was installed (:func:`how_installed`):
13
+
14
+ * **a downloaded app** cannot overwrite itself while it runs, so it
15
+ downloads and unpacks the new version, writes a small script that waits
16
+ for the app to quit, swaps the old app for the new one (keeping the old
17
+ one until the new one is in place) and starts it, and then quits;
18
+ * **a pip install** runs ``pip install --upgrade "openprocess[app]"`` in
19
+ the same Python and restarts;
20
+ * **a checkout** (``git clone`` + ``pip install -e``) runs ``git pull`` and
21
+ the editable install again and restarts, but only when the checkout has
22
+ no uncommitted changes; a copy without ``.git`` is only told what to run.
23
+
24
+ A release without a download for this system (as 0.7.0 was for CPNpy, whose
25
+ files have another name) can only be opened on the release page.
18
26
 
19
27
  Everything but :class:`UpdateDialog` (at the end) works without a window.
20
28
  """
@@ -96,6 +104,83 @@ def installed_app() -> Path | None:
96
104
  return executable.parent # OpenProcess/OpenProcess(.exe)
97
105
 
98
106
 
107
+ @dataclass(frozen=True)
108
+ class Install:
109
+ """How this copy of OpenProcess was installed, which decides how it updates."""
110
+
111
+ #: ``"app"``: a downloaded bundle; ``"source"``: a checkout; ``"pip"``: a package.
112
+ kind: str
113
+ #: The bundle, the checkout's folder, or the package's folder.
114
+ path: Path
115
+ #: A checkout with a ``.git`` folder, so ``git pull`` works.
116
+ git: bool = False
117
+
118
+
119
+ def _package_dir() -> Path:
120
+ from ... import __file__ as package_init
121
+ return Path(package_init).resolve().parent
122
+
123
+
124
+ def how_installed() -> Install:
125
+ app = installed_app()
126
+ if app is not None:
127
+ return Install("app", app)
128
+ package = _package_dir()
129
+ root = package.parent
130
+ if (root / "pyproject.toml").is_file():
131
+ return Install("source", root, git=(root / ".git").exists())
132
+ return Install("pip", package)
133
+
134
+
135
+ def update_way(install: Install, release: "Release") -> str | None:
136
+ """How ``release`` can be installed over ``install``: ``"app"`` (download and
137
+ swap), ``"pip"`` (pip in the same environment), ``"source"`` (``git pull``
138
+ and the editable install again), or None (only the release page helps)."""
139
+ if install.kind == "app":
140
+ return "app" if release.download_for() is not None else None
141
+ if install.kind == "pip":
142
+ return "pip"
143
+ if install.kind == "source" and install.git:
144
+ return "source"
145
+ return None
146
+
147
+
148
+ def update_commands(install: Install, release: "Release", way: str) -> list[list[str]]:
149
+ """The commands that update a pip install or a checkout, in order."""
150
+ python = sys.executable
151
+ if way == "pip":
152
+ return [[python, "-m", "pip", "install", "--upgrade", f"openprocess[app]=={release.version}"]]
153
+ if way == "source":
154
+ return [["git", "-C", str(install.path), "pull", "--ff-only"],
155
+ [python, "-m", "pip", "install", "-e", f"{install.path}[app]"]]
156
+ raise ValueError(f"no commands for {way!r}")
157
+
158
+
159
+ def run_update(install: Install, release: "Release", way: str, run=subprocess.run) -> str:
160
+ """Run the update commands; their output, or :class:`RuntimeError` with it."""
161
+ if way == "source":
162
+ status = run(["git", "-C", str(install.path), "status", "--porcelain"],
163
+ capture_output=True, text=True)
164
+ if status.returncode == 0 and status.stdout.strip():
165
+ raise RuntimeError("The checkout has uncommitted changes, which git pull would have "
166
+ "to merge. Commit or stash them, then update again.")
167
+ output: list[str] = []
168
+ for command in update_commands(install, release, way):
169
+ output.append("$ " + " ".join(command))
170
+ result = run(command, capture_output=True, text=True)
171
+ output.append(((result.stdout or "") + (result.stderr or "")).strip())
172
+ if result.returncode != 0:
173
+ raise RuntimeError("\n".join(output).strip())
174
+ return "\n".join(output).strip()
175
+
176
+
177
+ def restart_command() -> list[str]:
178
+ """The command that starts this OpenProcess again (after a pip or source update)."""
179
+ if getattr(sys, "frozen", False):
180
+ return [sys.executable] + sys.argv[1:]
181
+ return [sys.executable] + sys.argv
182
+
183
+
99
184
  # ---------------------------------------------------------------------------
100
185
  # The latest release
101
186
  # ---------------------------------------------------------------------------
@@ -349,11 +434,18 @@ class UpdateDialog(QDialog):
349
434
 
350
435
  INSTALL, LATER, SKIP, PAGE = range(4)
351
436
 
352
- def __init__(self, release: Release, can_install: bool, parent=None) -> None:
437
+ def __init__(self, release: Release, can_install: bool, parent=None,
438
+ install: Install | None = None, way: str | None = None) -> None:
353
439
  super().__init__(parent)
354
440
  self.setWindowTitle("Software Update")
355
441
  self.setMinimumSize(520, 420)
356
442
  self.outcome = self.LATER
443
+ install = install or how_installed()
444
+ if way is None and can_install:
445
+ way = update_way(install, release) or "app"
446
+ if not can_install:
447
+ way = None
448
+ self.way = way
357
449
  layout = QVBoxLayout(self)
358
450
  layout.setSpacing(10)
359
451
  heading = QLabel(f"<b>OpenProcess {release.version} is available</b> — you have "
@@ -364,18 +456,21 @@ class UpdateDialog(QDialog):
364
456
  notes.setOpenExternalLinks(True)
365
457
  notes.setMarkdown(release.notes or "_No release notes._")
366
458
  layout.addWidget(notes, 1)
367
- if not can_install:
368
- hint = QLabel("You are running OpenProcess from its source code, which it does not "
369
- "change by itself. To update, run <code>git pull</code> in your "
370
- "OpenProcess folder, then <code>conda env update -f environment.yml "
371
- "--prune</code> (or <code>pip install -e \".[app]\"</code>).")
459
+ text = update_hint(install, release, way)
460
+ if text:
461
+ hint = QLabel(text)
372
462
  hint.setWordWrap(True)
463
+ hint.setOpenExternalLinks(True)
373
464
  layout.addWidget(hint)
374
465
  buttons = QDialogButtonBox()
375
- if can_install:
376
- install = buttons.addButton("Download && Install", QDialogButtonBox.AcceptRole)
377
- install.clicked.connect(lambda: self._finish(self.INSTALL))
378
- install.setDefault(True)
466
+ if way == "app":
467
+ action = buttons.addButton("Download && Install", QDialogButtonBox.AcceptRole)
468
+ action.clicked.connect(lambda: self._finish(self.INSTALL))
469
+ action.setDefault(True)
470
+ elif way in ("pip", "source"):
471
+ action = buttons.addButton("Update && Restart", QDialogButtonBox.AcceptRole)
472
+ action.clicked.connect(lambda: self._finish(self.INSTALL))
473
+ action.setDefault(True)
379
474
  else:
380
475
  page = buttons.addButton("Open Release Page", QDialogButtonBox.AcceptRole)
381
476
  page.clicked.connect(lambda: self._finish(self.PAGE))
@@ -393,6 +488,30 @@ class UpdateDialog(QDialog):
393
488
  self.reject()
394
489
 
395
490
 
491
+ def update_hint(install: Install, release: Release, way: str | None) -> str:
492
+ """What the dialog says under the notes: how this copy updates, or why it cannot."""
493
+ if way == "app":
494
+ return ""
495
+ if way == "pip":
496
+ return ("You installed OpenProcess with pip. <i>Update &amp; Restart</i> runs "
497
+ "<code>pip install --upgrade \"openprocess[app]\"</code> in this same Python, then "
498
+ "starts OpenProcess again.")
499
+ if way == "source":
500
+ return (f"You are running OpenProcess from a checkout ({install.path}). <i>Update &amp; "
501
+ "Restart</i> runs <code>git pull</code> there and <code>pip install -e "
502
+ "\".[app]\"</code> in this Python (a conda environment keeps working), then starts "
503
+ "OpenProcess again. It refuses while the checkout has uncommitted changes.")
504
+ if install.kind == "app":
505
+ return (f"This release has no download for {system_name()} ({architecture()}), so it "
506
+ "cannot be installed from here. The release page shows what it has.")
507
+ if install.kind == "source":
508
+ return (f"You are running OpenProcess from a copy of its source ({install.path}) that is "
509
+ "not a git checkout, so it cannot update itself. Get the new version from the "
510
+ "release page, or clone the repository and run <code>pip install -e "
511
+ "\".[app]\"</code>.")
512
+ return ""
513
+
514
+
396
515
  class UpdateBar(QFrame):
397
516
  """A slim bar at the top of the window: a new version is out.
398
517
 
@@ -431,9 +550,15 @@ class UpdateBar(QFrame):
431
550
  self.release: Release | None = None
432
551
  self.setVisible(False)
433
552
 
434
- def offer(self, release: Release, can_install: bool) -> None:
553
+ def offer(self, release: Release, can_install: bool, way: str | None = None) -> None:
435
554
  self.release = release
436
555
  self.text.setText(f"<b>OpenProcess {release.version} is available</b> — you have "
437
556
  f"{__version__}.")
438
- self.install_button.setText("Install Now" if can_install else "How to Update")
557
+ if not can_install:
558
+ label = "How to Update"
559
+ elif way in ("pip", "source"):
560
+ label = "Update Now"
561
+ else:
562
+ label = "Install Now"
563
+ self.install_button.setText(label)
439
564
  self.setVisible(True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Process mining, Petri nets and workflows in one open app: every algorithm visible, every result reproducible, every step teachable
5
5
  Author: Sam Struthers
6
6
  License: MIT
@@ -288,6 +288,8 @@ def test_a_new_version_at_launch_is_a_bar_not_a_dialog(monkeypatch):
288
288
  window.show()
289
289
  monkeypatch.setattr(window, "_install_update", installs.append)
290
290
  monkeypatch.setattr(window, "_can_install", lambda release: True)
291
+ monkeypatch.setattr(window, "_update_way", lambda release: (
292
+ updates.Install("app", Path("/Applications/OpenProcess.app")), "app"))
291
293
  bar = window.update_bar
292
294
 
293
295
  def check():
@@ -326,6 +328,8 @@ def test_running_from_source_only_says_how_to_update(monkeypatch):
326
328
 
327
329
  application = QApplication.instance() or QApplication([])
328
330
  monkeypatch.delattr(sys, "frozen", raising=False)
331
+ # A copy of the source without .git: nothing the app can run for you.
332
+ monkeypatch.setattr(updates, "how_installed", lambda: updates.Install("source", ROOT, git=False))
329
333
  shown, installs = [], []
330
334
 
331
335
  def fake_exec(dialog):
@@ -348,11 +352,108 @@ def test_running_from_source_only_says_how_to_update(monkeypatch):
348
352
  bar.install_button.click()
349
353
  _pump(application, 0.1)
350
354
  assert installs == [] and len(shown) == 1
351
- assert "git pull" in " ".join(label.text() for label in shown[0].findChildren(
355
+ assert "not a git checkout" in " ".join(label.text() for label in shown[0].findChildren(
352
356
  updates.QLabel))
353
357
  window.close()
354
358
 
355
359
 
360
+ def test_how_this_copy_was_installed_decides_how_it_updates(monkeypatch, tmp_path):
361
+ release = Release.from_github(dict(GITHUB_ANSWER, tag_name="v99.0.0"))
362
+ # A downloaded app: the download, when the release has one for this system.
363
+ monkeypatch.setattr(updates, "installed_app", lambda: tmp_path / "OpenProcess.app")
364
+ app = updates.how_installed()
365
+ assert app.kind == "app" and updates.update_way(app, release) == "app"
366
+ none = Release.from_github(dict(GITHUB_ANSWER, tag_name="v99.0.0", assets=[]))
367
+ assert updates.update_way(app, none) is None
368
+ assert "no download for" in updates.update_hint(app, none, None)
369
+ # A checkout: git pull and the editable install; without .git, only advice.
370
+ monkeypatch.setattr(updates, "installed_app", lambda: None)
371
+ checkout = tmp_path / "src"
372
+ (checkout / "openprocess").mkdir(parents=True)
373
+ (checkout / "pyproject.toml").write_text("[project]\nname = 'openprocess'\n")
374
+ monkeypatch.setattr(updates, "_package_dir", lambda: checkout / "openprocess")
375
+ copy = updates.how_installed()
376
+ assert copy.kind == "source" and not copy.git and updates.update_way(copy, release) is None
377
+ (checkout / ".git").mkdir()
378
+ source = updates.how_installed()
379
+ assert source.git and updates.update_way(source, release) == "source"
380
+ commands = updates.update_commands(source, release, "source")
381
+ assert commands[0][:4] == ["git", "-C", str(checkout), "pull"]
382
+ assert commands[1][-2:] == ["-e", f"{checkout}[app]"]
383
+ # A pip install: pip, pinned to the release.
384
+ site = tmp_path / "site-packages" / "openprocess"
385
+ site.mkdir(parents=True)
386
+ monkeypatch.setattr(updates, "_package_dir", lambda: site)
387
+ pip = updates.how_installed()
388
+ assert pip.kind == "pip" and updates.update_way(pip, release) == "pip"
389
+ assert updates.update_commands(pip, release, "pip")[0][-1] == "openprocess[app]==99.0.0"
390
+ assert "pip install --upgrade" in updates.update_hint(pip, release, "pip")
391
+ assert updates.restart_command()[0] == sys.executable
392
+
393
+
394
+ def test_run_update_runs_the_commands_and_refuses_a_dirty_checkout(tmp_path):
395
+ release = Release.from_github(dict(GITHUB_ANSWER, tag_name="v99.0.0"))
396
+ source = updates.Install("source", tmp_path, git=True)
397
+ calls = []
398
+
399
+ class Done:
400
+ def __init__(self, code=0, out="ok"):
401
+ self.returncode, self.stdout, self.stderr = code, out, ""
402
+
403
+ def fake_run(command, **kwargs):
404
+ calls.append(command)
405
+ if command[3:] == ["status", "--porcelain"]:
406
+ return Done(out=" M changed.py\n" if fake_run.dirty else "")
407
+ return Done(code=1, out="boom") if fake_run.fail else Done()
408
+ fake_run.dirty, fake_run.fail = True, False
409
+ with pytest.raises(RuntimeError, match="uncommitted changes"):
410
+ updates.run_update(source, release, "source", run=fake_run)
411
+ assert len(calls) == 1 # nothing was pulled
412
+ fake_run.dirty = False
413
+ output = updates.run_update(source, release, "source", run=fake_run)
414
+ assert calls[2][3] == "pull" and "pip" in calls[3][2]
415
+ assert output.startswith("$ git -C") and "ok" in output
416
+ fake_run.fail = True
417
+ with pytest.raises(RuntimeError, match="boom"):
418
+ updates.run_update(updates.Install("pip", tmp_path), release, "pip", run=fake_run)
419
+
420
+
421
+ def test_a_pip_install_updates_with_pip_and_offers_a_restart(monkeypatch):
422
+ from PySide6.QtWidgets import QApplication
423
+
424
+ from openprocess.gui.studio.app import StudioWindow
425
+
426
+ application = QApplication.instance() or QApplication([])
427
+ monkeypatch.setattr(updates, "how_installed", lambda: updates.Install("pip", ROOT))
428
+ monkeypatch.setattr(updates, "fetch_latest", lambda: Release.from_github(
429
+ dict(GITHUB_ANSWER, tag_name="v99.0.0")))
430
+ ran, after = [], []
431
+ monkeypatch.setattr(updates, "run_update", lambda install, release, way: (
432
+ ran.append((install.kind, way)) or "$ pip install …\nSuccessfully installed"))
433
+ window = StudioWindow()
434
+ window.show()
435
+ monkeypatch.setattr(window, "_after_update", lambda release, output: after.append(output))
436
+ window.check_for_updates(manual=False)
437
+ for _ in range(300):
438
+ if window.update_bar.isVisible():
439
+ break
440
+ _pump(application, 0.01)
441
+ bar = window.update_bar
442
+ assert bar.isVisible() and bar.install_button.text() == "Update Now"
443
+ bar.install_button.click()
444
+ for _ in range(300):
445
+ if after:
446
+ break
447
+ _pump(application, 0.01)
448
+ assert ran == [("pip", "pip")] and "Successfully installed" in after[0]
449
+ dialog = updates.UpdateDialog(Release.from_github(dict(GITHUB_ANSWER, tag_name="v99.0.0")),
450
+ True, None, updates.Install("pip", ROOT), "pip")
451
+ texts = [b.text() for b in dialog.findChildren(updates.QPushButton)]
452
+ assert "Update && Restart" in texts
453
+ assert "pip install --upgrade" in " ".join(l.text() for l in dialog.findChildren(updates.QLabel))
454
+ window.close()
455
+
456
+
356
457
  def test_release_notes_script_writes_utf8_on_windows_too(tmp_path):
357
458
  """Regression (v0.3.2 build): printing "✕" failed on Windows, whose stdout
358
459
  encoding is cp1252. Run the script with that encoding to check."""
File without changes
File without changes