pyqt-code-editor 1.0.4__tar.gz → 1.0.6__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 (91) hide show
  1. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/PKG-INFO +1 -1
  2. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/__init__.py +1 -1
  3. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/sigmund/sigmund_analyst_widget.py +1 -1
  4. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/base.py +7 -0
  5. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/manager.py +91 -13
  6. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/COPYING +0 -0
  7. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyproject.toml +0 -0
  8. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/_settings.py +0 -0
  9. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/app.py +0 -0
  10. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/code_editors/__init__.py +0 -0
  11. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/code_editors/code_editors.py +0 -0
  12. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  13. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/code_editors/languages/generic.py +0 -0
  14. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/code_editors/languages/javascript.py +0 -0
  15. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
  16. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/code_editors/languages/python.py +0 -0
  17. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/code_editors/languages/text.py +0 -0
  18. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/__init__.py +0 -0
  19. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/editor_panel.py +0 -0
  20. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/find_in_files.py +0 -0
  21. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/jupyter_console/__init__.py +0 -0
  22. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/jupyter_console/jupyter_console.py +0 -0
  23. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/jupyter_console/jupyter_console_tab.py +0 -0
  24. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/jupyter_console/kernel_spec_manager.py +0 -0
  25. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/project_explorer.py +0 -0
  26. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/settings_panel.py +0 -0
  27. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/sigmund/__init__.py +0 -0
  28. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/sigmund/confirm_run_code_dialog.py +0 -0
  29. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/sigmund/editor_workspace.py +0 -0
  30. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/sigmund/sigmund.py +0 -0
  31. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/sigmund/sigmund_analyst_chat_widget.py +0 -0
  32. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/components/workspace_explorer.py +0 -0
  33. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/environment_manager.py +0 -0
  34. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/__init__.py +0 -0
  35. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/auto_indent.py +0 -0
  36. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/auto_pair.py +0 -0
  37. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/check.py +0 -0
  38. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/comment.py +0 -0
  39. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/complete.py +0 -0
  40. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/execute.py +0 -0
  41. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/file_link.py +0 -0
  42. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/highlight_matching_brackets.py +0 -0
  43. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/line_number.py +0 -0
  44. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/merge_undo_actions.py +0 -0
  45. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/python_auto_indent.py +0 -0
  46. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/search_replace.py +0 -0
  47. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/shortcuts.py +0 -0
  48. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
  49. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/symbols.py +0 -0
  50. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/theme.py +0 -0
  51. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  52. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/mixins/zoom.py +0 -0
  53. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/sigmund-analyst.png +0 -0
  54. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/signal_router.py +0 -0
  55. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  56. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  57. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
  58. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
  59. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
  60. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/themes.py +0 -0
  61. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/utils/__init__.py +0 -0
  62. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/utils/languages/__init__.py +0 -0
  63. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/utils/languages/python/__init__.py +0 -0
  64. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/utils/languages/python/_auto_indent.py +0 -0
  65. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/utils/languages/python/_extract_cells.py +0 -0
  66. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/utils/languages/python/_mask_str_in_code.py +0 -0
  67. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/watchdog.py +0 -0
  68. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/__init__.py +0 -0
  69. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
  70. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/completion_popup.py +0 -0
  71. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/dock.py +0 -0
  72. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
  73. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
  74. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
  75. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
  76. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
  77. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
  78. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/__init__.py +0 -0
  79. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/languages/__init__.py +0 -0
  80. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/languages/generic.py +0 -0
  81. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/languages/python.py +0 -0
  82. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/process.py +0 -0
  83. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/providers/__init__.py +0 -0
  84. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/providers/codestral.py +0 -0
  85. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/providers/jedi.py +0 -0
  86. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/providers/ruff.py +0 -0
  87. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
  88. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/providers/symbol.py +0 -0
  89. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/providers/tree_sitter.py +0 -0
  90. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/pyqt_code_editor/worker/settings.py +0 -0
  91. {pyqt_code_editor-1.0.4 → pyqt_code_editor-1.0.6}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyqt_code_editor
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Fully featured code-editor widgets for PyQt
5
5
  Keywords: code editor,pyqt,ide
6
6
  Author-email: Sebastiaan Mathôt <s.mathot@cogsci.nl>
@@ -2,4 +2,4 @@
2
2
 
3
3
  from ._settings import settings
4
4
 
5
- __version__ = '1.0.4'
5
+ __version__ = '1.0.6'
@@ -174,7 +174,7 @@ class SigmundAnalystWidget(SigmundWidget):
174
174
  time.sleep(.5)
175
175
  self._execution_result = None
176
176
  self._execution_loop = QEventLoop()
177
- QTimer.singleShot(30000, self._execution_loop.quit) # 30 second timeout
177
+ QTimer.singleShot(5 * 60000, self._execution_loop.quit) # 5min timeout
178
178
  self._jupyter_console.execute_code(code)
179
179
  self._execution_loop.exec_()
180
180
  self._execution_loop = None
@@ -146,6 +146,13 @@ class Base:
146
146
  # This function periodically prunes worker processes if they are unused.
147
147
  for pid in to_remove:
148
148
  self._active_requests.pop(pid, None)
149
+ # Non-blockingly finish cleaning up any workers that were
150
+ # previously asked to stop, and possibly ask further idle
151
+ # workers to stop (throttled internally). Neither call blocks
152
+ # the GUI thread, even on platforms (e.g. Windows) where
153
+ # joining a worker process can take a noticeable amount of
154
+ # time.
155
+ manager.reap_stopping_workers()
149
156
  manager.stop_unused_workers()
150
157
 
151
158
  def set_modified(self, modified):
@@ -5,8 +5,11 @@ from .process import main_worker_process_function
5
5
  from .. import watchdog, settings
6
6
  logger = logging.getLogger(__name__)
7
7
  STOP_UNUSED_INTERVAL = 10
8
+ MAX_CONCURRENT_STARTING = 2
8
9
  _last_stop_unused_time = 0
9
10
  _workers = {} # pid -> {"process", "request_queue", "result_queue", "is_free"}
11
+ _stopping = {} # pid -> same shape as _workers, but already asked to quit
12
+ _starting = set() # pids of workers created but not yet proven alive-and-useful
10
13
  suspended = False
11
14
 
12
15
 
@@ -35,6 +38,9 @@ def _cleanup_dead_workers():
35
38
  w["process"].join(timeout=1)
36
39
  except Exception as e:
37
40
  logger.warning(f"Error joining dead worker {pid}: {e}")
41
+ # If this worker died before ever completing its first request, it
42
+ # was still occupying a "starting" slot; free that slot up.
43
+ _starting.discard(pid)
38
44
 
39
45
 
40
46
  def send_worker_request(**data) -> (Queue, int):
@@ -46,7 +52,17 @@ def send_worker_request(**data) -> (Queue, int):
46
52
  call mark_worker_as_free(pid) to release this worker for future use.
47
53
 
48
54
  If workers are suspended, return (None, None).
49
- """
55
+
56
+ We also decline to create a new worker (returning (None, None)) if
57
+ MAX_CONCURRENT_STARTING workers are already "starting" -- i.e. have
58
+ been created but haven't yet completed a first successful round-trip
59
+ (see _starting / mark_worker_as_free()). This caps how many freshly
60
+ spawned, still-booting worker processes can pile up at once. A single
61
+ worker creation still pays the normal (and, on Windows, sometimes
62
+ substantial) process-creation cost on the GUI thread, but we avoid the
63
+ much worse case of several such creations happening back-to-back and
64
+ competing for CPU/disk/antivirus scanning at the same time.
65
+ """
50
66
  if suspended:
51
67
  return None, None
52
68
  # 1. Look for an existing free worker
@@ -57,7 +73,12 @@ def send_worker_request(**data) -> (Queue, int):
57
73
  w["request_queue"].put(data)
58
74
  return w["result_queue"], pid
59
75
 
60
- # 2. If no free worker was found, create a new one.
76
+ # 2. If no free worker was found, create a new one -- unless too many
77
+ # workers are already in the process of starting up.
78
+ if len(_starting) >= MAX_CONCURRENT_STARTING:
79
+ logger.info("declining requst, because too many worker(s) already starting.")
80
+ return None, None
81
+
61
82
  request_queue = Queue()
62
83
  result_queue = Queue()
63
84
  p = Process(target=main_worker_process_function,
@@ -72,6 +93,7 @@ def send_worker_request(**data) -> (Queue, int):
72
93
  "result_queue": result_queue,
73
94
  "is_free": False,
74
95
  }
96
+ _starting.add(pid)
75
97
 
76
98
  logger.info(f"Creating new worker {pid} for request {data['action']}")
77
99
  settings_action = {'action': 'set_settings',
@@ -85,12 +107,18 @@ def mark_worker_as_free(pid: int):
85
107
  """
86
108
  Mark a previously-used worker process (identified by pid)
87
109
  as free for reuse. If the worker has died, clean it up instead.
110
+
111
+ This is also the point at which a worker is considered to have proven
112
+ itself alive-and-useful (it has completed a request), so we release
113
+ its "starting" slot here if it still held one -- whether it succeeded
114
+ or died in the process.
88
115
  """
89
116
  w = _workers.get(pid)
90
117
  if w is None:
91
118
  return
92
119
  if w["process"].is_alive():
93
120
  w["is_free"] = True
121
+ _starting.discard(pid)
94
122
  logger.info(f"Marking worker {pid} as free")
95
123
  else:
96
124
  # The worker died while handling a request; clean up its resources
@@ -98,16 +126,51 @@ def mark_worker_as_free(pid: int):
98
126
  _workers.pop(pid)
99
127
  _close_worker_queues(w)
100
128
  w["process"].join(timeout=1)
129
+ _starting.discard(pid)
101
130
 
102
131
  def check_worker_alive(pid: int) -> bool:
103
132
  w = _workers.get(pid)
104
133
  return w and w["process"].is_alive()
105
134
 
106
- def stop_unused_workers(max_free: int = 1, force: bool = False):
135
+ def reap_stopping_workers():
136
+ """
137
+ Finish cleaning up worker processes that were previously asked to quit
138
+ (via stop_unused_workers()), but only once they have actually
139
+ terminated. This is non-blocking: we merely check `is_alive()` for each
140
+ stopping worker, and only `join()` (with a zero timeout, so it returns
141
+ essentially instantly) processes that have already exited. Workers that
142
+ haven't exited yet are left in place and will be picked up on a later
143
+ call.
144
+
145
+ This should be called periodically (e.g. from the same timer that calls
146
+ stop_unused_workers()) so that workers asked to stop are eventually
147
+ cleaned up and their queues closed.
148
+ """
149
+ done_pids = [pid for pid, w in _stopping.items() if not w["process"].is_alive()]
150
+ for pid in done_pids:
151
+ w = _stopping.pop(pid)
152
+ logger.info(f"Reaping stopped worker {pid}.")
153
+ try:
154
+ w["process"].join(timeout=0)
155
+ except Exception as e:
156
+ logger.warning(f"Error joining stopped worker {pid}: {e}")
157
+ _close_worker_queues(w)
158
+
159
+ def stop_unused_workers(max_free: int = 3, force: bool = False):
107
160
  """
108
- Stop free worker processes until there is at most 'max_free' free processes
109
- left. This keeps us from accumulating too many idle worker processes. Also
110
- prunes any workers that have died unexpectedly.
161
+ Request that free worker processes stop until there is at most
162
+ 'max_free' free processes left. This keeps us from accumulating too
163
+ many idle worker processes. Also prunes any workers that have died
164
+ unexpectedly.
165
+
166
+ Important: this function never blocks waiting for a worker to actually
167
+ exit. Workers that we decide to stop are sent a "quit" message and
168
+ moved into an internal `_stopping` bookkeeping dict; they are only
169
+ joined (and their queues closed) once they have actually terminated,
170
+ which is handled by reap_stopping_workers(). This matters because
171
+ joining a process that hasn't exited yet can block for a noticeable
172
+ amount of time, especially on Windows, and this function is typically
173
+ called from a periodic timer on the main/GUI thread.
111
174
  """
112
175
  global _last_stop_unused_time
113
176
  if not force and time.time() - _last_stop_unused_time < STOP_UNUSED_INTERVAL:
@@ -125,21 +188,24 @@ def stop_unused_workers(max_free: int = 1, force: bool = False):
125
188
  logger.info("No free workers to stop.")
126
189
  return
127
190
 
128
- # Stop some free workers until we have exactly max_free left
191
+ # Ask some free workers to stop until we have max_free left. We don't
192
+ # wait for them to exit here; see reap_stopping_workers().
129
193
  while to_stop > 0 and free_pids:
130
194
  pid = free_pids.pop()
131
195
  w = _workers.pop(pid)
132
- logger.info(f"Stopping free worker {pid} because we have too many.")
196
+ logger.info(f"Asking free worker {pid} to stop because we have too many.")
133
197
  w["request_queue"].put({"action": "quit"})
134
- w["process"].join()
135
- _close_worker_queues(w)
198
+ _stopping[pid] = w
136
199
  to_stop -= 1
137
200
 
138
- logger.info("Finished stopping unused workers.")
201
+ logger.info("Finished requesting unused workers to stop.")
139
202
 
140
203
  def stop_all_workers():
141
204
  """
142
- Cleanly shut down all worker processes.
205
+ Cleanly shut down all worker processes. Unlike stop_unused_workers(),
206
+ this function blocks until every worker has actually exited, since it
207
+ is only used when we need a hard guarantee that no worker processes
208
+ remain (e.g. on application shutdown or when suspending).
143
209
  """
144
210
  logger.info(f"Stopping {len(_workers)} worker processes...")
145
211
  for pid, w in list(_workers.items()):
@@ -149,6 +215,18 @@ def stop_all_workers():
149
215
  w["process"].join()
150
216
  _close_worker_queues(w)
151
217
  del _workers[pid]
218
+ # Also finish off any workers that were previously asked to stop (via
219
+ # stop_unused_workers()) but haven't been reaped yet.
220
+ for pid, w in list(_stopping.items()):
221
+ logger.info(f"Joining previously-stopping worker {pid}.")
222
+ try:
223
+ w["process"].join()
224
+ except Exception as e:
225
+ logger.warning(f"Error joining stopping worker {pid}: {e}")
226
+ _close_worker_queues(w)
227
+ del _stopping[pid]
228
+ # Nothing is left running, so no worker can still be "starting".
229
+ _starting.clear()
152
230
  logger.info("All workers stopped.")
153
231
 
154
232
 
@@ -177,4 +255,4 @@ def resume():
177
255
  logger.info("Resuming worker processes...")
178
256
 
179
257
 
180
- settings.setting_changed.connect(update_setting)
258
+ settings.setting_changed.connect(update_setting)