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.
- {openprocess-0.7.0/openprocess.egg-info → openprocess-0.7.1}/PKG-INFO +1 -1
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/__init__.py +1 -1
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/box.py +8 -3
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/app.py +77 -6
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/updates.py +143 -18
- {openprocess-0.7.0 → openprocess-0.7.1/openprocess.egg-info}/PKG-INFO +1 -1
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_updates.py +102 -1
- {openprocess-0.7.0 → openprocess-0.7.1}/LICENSE +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/README.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/cpnpy/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/analysis/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/analysis/state_space.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/analysis/state_space_process.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/cli.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/exercises/pack.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/check.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/compare.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/cpn.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/discover.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/filter.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/input.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/output.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/predict.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/science.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/boxes/sweeps.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/convert.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/datasets.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/explain.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/library.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/record.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/runner.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/sweep.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/types.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/flow/workflow.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/app.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/arc_editing.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/canvas.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/canvas.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/page.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/templates.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/flow/viewers.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/items.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/learn/answer_boxes.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/learn/concealment.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/learn/mode.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/panning.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/resources/openprocess-icon.png +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/__main__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/charts.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/compare_page.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/cpn_page.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/definition_view.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/derivation_view.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/documents.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/dotted_chart.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/file_dialogs.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/filter_dialog.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/graph_builders.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/graph_view.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/instances.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/log_editor.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/log_page.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/markdown_view.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/mathtext.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/ml_highlighter.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/model_page.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/net_comparison.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/notes_overlay.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/petri_page.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/regions_view.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/sidebar.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/style.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/tool_icons.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/widgets.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/workers.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/studio/workspace.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/theme.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/gui/tidy.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/io/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/io/cpn_reader.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/io/cpn_writer.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/answers.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/checks.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/computed.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/context.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/exam.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/exercise-packs.md +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/importer.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/notation.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/pack.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/learn/sheet.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/analysis.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/columns.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/compare_nets.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/conformance/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/conformance/alignments.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/conformance/quality.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/conformance/token_replay.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/csv_import.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/definitions.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/dfg.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/alpha.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/heuristics.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/inductive.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/discovery/state_regions.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/filtering.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/footprint.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/invariants.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/layout.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/log.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/petrinet.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/playout.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/pm4py_bridge.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/pnml.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/processtree.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/regions.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/stats.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/structure.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/transition_system.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/mining/xes.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/ast_nodes.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/builtins.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/colorsets.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/errors.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/evaluator.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/lexer.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/multiset.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/parser.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/ml/values.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/declarations.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/examples.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/net.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/model/plain.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/references.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/sim/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/sim/binding.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/sim/export.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/sim/simulator.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/__init__.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/answers.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/checks.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/pack.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess/teaching/sheet.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/SOURCES.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/dependency_links.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/entry_points.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/requires.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/openprocess.egg-info/top_level.txt +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/pyproject.toml +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/setup.cfg +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_arc_editing.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_colorsets.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_columns.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_cpn_course_model.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_definitions.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_exercises_gui.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_flow.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_flow_gui.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_hierarchy.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_io.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_learn.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_learn_gui.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_log_editing.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_mining.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_ml.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_multiset.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_regions.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_rename.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_simulation.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_state_space.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_studio_gui.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_teaching.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_tidy.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_wf_structure.py +0 -0
- {openprocess-0.7.0 → openprocess-0.7.1}/tests/test_workspace.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
380
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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.
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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 & 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 & "
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|