pyqt-code-editor 1.0.5__tar.gz → 1.1.0__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.5 → pyqt_code_editor-1.1.0}/PKG-INFO +1 -1
  2. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/__init__.py +1 -1
  3. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/manager.py +109 -23
  4. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/COPYING +0 -0
  5. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyproject.toml +0 -0
  6. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/_settings.py +0 -0
  7. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/app.py +0 -0
  8. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/code_editors/__init__.py +0 -0
  9. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/code_editors/code_editors.py +0 -0
  10. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  11. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/code_editors/languages/generic.py +0 -0
  12. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/code_editors/languages/javascript.py +0 -0
  13. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/code_editors/languages/markdown.py +0 -0
  14. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/code_editors/languages/python.py +0 -0
  15. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/code_editors/languages/text.py +0 -0
  16. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/__init__.py +0 -0
  17. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/editor_panel.py +0 -0
  18. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/find_in_files.py +0 -0
  19. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/jupyter_console/__init__.py +0 -0
  20. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/jupyter_console/jupyter_console.py +0 -0
  21. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/jupyter_console/jupyter_console_tab.py +0 -0
  22. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/jupyter_console/kernel_spec_manager.py +0 -0
  23. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/project_explorer.py +0 -0
  24. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/settings_panel.py +0 -0
  25. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/sigmund/__init__.py +0 -0
  26. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/sigmund/confirm_run_code_dialog.py +0 -0
  27. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/sigmund/editor_workspace.py +0 -0
  28. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/sigmund/sigmund.py +0 -0
  29. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/sigmund/sigmund_analyst_chat_widget.py +0 -0
  30. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/sigmund/sigmund_analyst_widget.py +0 -0
  31. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/components/workspace_explorer.py +0 -0
  32. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/environment_manager.py +0 -0
  33. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/__init__.py +0 -0
  34. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/auto_indent.py +0 -0
  35. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/auto_pair.py +0 -0
  36. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/base.py +0 -0
  37. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/check.py +0 -0
  38. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/comment.py +0 -0
  39. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/complete.py +0 -0
  40. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/execute.py +0 -0
  41. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/file_link.py +0 -0
  42. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/highlight_matching_brackets.py +0 -0
  43. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/line_number.py +0 -0
  44. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/merge_undo_actions.py +0 -0
  45. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/python_auto_indent.py +0 -0
  46. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/search_replace.py +0 -0
  47. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/shortcuts.py +0 -0
  48. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/smart_backspace_delete.py +0 -0
  49. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/symbols.py +0 -0
  50. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/theme.py +0 -0
  51. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  52. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/mixins/zoom.py +0 -0
  53. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/sigmund-analyst.png +0 -0
  54. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/signal_router.py +0 -0
  55. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  56. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  57. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/syntax_highlighters/languages/generic.py +0 -0
  58. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/syntax_highlighters/languages/python.py +0 -0
  59. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +0 -0
  60. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/themes.py +0 -0
  61. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/utils/__init__.py +0 -0
  62. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/utils/languages/__init__.py +0 -0
  63. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/utils/languages/python/__init__.py +0 -0
  64. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/utils/languages/python/_auto_indent.py +0 -0
  65. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/utils/languages/python/_extract_cells.py +0 -0
  66. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/utils/languages/python/_mask_str_in_code.py +0 -0
  67. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/watchdog.py +0 -0
  68. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/__init__.py +0 -0
  69. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/calltip_widget.py +0 -0
  70. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/completion_popup.py +0 -0
  71. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/dock.py +0 -0
  72. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/quick_open_dialog.py +0 -0
  73. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/quick_open_file_dialog.py +0 -0
  74. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/quick_symbol_dialog.py +0 -0
  75. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/search_replace_frame.py +0 -0
  76. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/tab_splitter.py +0 -0
  77. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/widgets/tabbed_editor.py +0 -0
  78. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/__init__.py +0 -0
  79. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/languages/__init__.py +0 -0
  80. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/languages/generic.py +0 -0
  81. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/languages/python.py +0 -0
  82. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/process.py +0 -0
  83. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/providers/__init__.py +0 -0
  84. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/providers/codestral.py +0 -0
  85. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/providers/jedi.py +0 -0
  86. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/providers/ruff.py +0 -0
  87. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/providers/sigmund.py +0 -0
  88. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/providers/symbol.py +0 -0
  89. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/providers/tree_sitter.py +0 -0
  90. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/pyqt_code_editor/worker/settings.py +0 -0
  91. {pyqt_code_editor-1.0.5 → pyqt_code_editor-1.1.0}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyqt_code_editor
3
- Version: 1.0.5
3
+ Version: 1.1.0
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.5'
5
+ __version__ = '1.1.0'
@@ -5,9 +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"}
10
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
11
13
  suspended = False
12
14
 
13
15
 
@@ -36,29 +38,24 @@ def _cleanup_dead_workers():
36
38
  w["process"].join(timeout=1)
37
39
  except Exception as e:
38
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)
39
44
 
40
45
 
41
- def send_worker_request(**data) -> (Queue, int):
46
+ def _start_new_worker(is_free: bool = False) -> int:
42
47
  """
43
- Send a request to a worker process. If a free worker is available,
44
- reuse it; otherwise create a new one. Return (result_queue, pid).
48
+ Create and start a single new worker process, send it the current
49
+ settings, and register it in _workers and _starting.
45
50
 
46
- The caller can poll the result_queue for responses, and once done,
47
- call mark_worker_as_free(pid) to release this worker for future use.
51
+ `is_free` controls whether the worker is immediately marked as
52
+ available for reuse (used by start_worker_pool(), which starts idle
53
+ workers ahead of time) or not (used by send_worker_request(), which
54
+ immediately queues a real request on it).
48
55
 
49
- If workers are suspended, return (None, None).
56
+ Returns the new worker's pid. Does not check MAX_CONCURRENT_STARTING;
57
+ callers are responsible for that.
50
58
  """
51
- if suspended:
52
- return None, None
53
- # 1. Look for an existing free worker
54
- for pid, w in list(_workers.items()):
55
- if w["is_free"] and w["process"].is_alive():
56
- logger.info(f"Reusing free worker {pid} (of {len(_workers)}) for request {list(data.keys())}")
57
- w["is_free"] = False
58
- w["request_queue"].put(data)
59
- return w["result_queue"], pid
60
-
61
- # 2. If no free worker was found, create a new one.
62
59
  request_queue = Queue()
63
60
  result_queue = Queue()
64
61
  p = Process(target=main_worker_process_function,
@@ -71,27 +68,108 @@ def send_worker_request(**data) -> (Queue, int):
71
68
  "process": p,
72
69
  "request_queue": request_queue,
73
70
  "result_queue": result_queue,
74
- "is_free": False,
71
+ "is_free": is_free,
75
72
  }
73
+ _starting.add(pid)
76
74
 
77
- logger.info(f"Creating new worker {pid} for request {data['action']}")
75
+ logger.info(f"Created new worker {pid} (is_free={is_free})")
78
76
  settings_action = {'action': 'set_settings',
79
77
  'settings': {name: value for name, value in settings}}
80
78
  request_queue.put(settings_action)
81
- # 3. Send the request, return the new worker's result queue and pid.
82
- request_queue.put(data)
83
- return result_queue, pid
79
+ return pid
80
+
81
+
82
+ def send_worker_request(**data) -> (Queue, int):
83
+ """
84
+ Send a request to a worker process. If a free worker is available,
85
+ reuse it; otherwise create a new one. Return (result_queue, pid).
86
+
87
+ The caller can poll the result_queue for responses, and once done,
88
+ call mark_worker_as_free(pid) to release this worker for future use.
89
+
90
+ If workers are suspended, return (None, None).
91
+
92
+ We also decline to create a new worker (returning (None, None)) if
93
+ MAX_CONCURRENT_STARTING workers are already "starting" -- i.e. have
94
+ been created but haven't yet completed a first successful round-trip
95
+ (see _starting / mark_worker_as_free()). This caps how many freshly
96
+ spawned, still-booting worker processes can pile up at once. A single
97
+ worker creation still pays the normal (and, on Windows, sometimes
98
+ substantial) process-creation cost on the GUI thread, but we avoid the
99
+ much worse case of several such creations happening back-to-back and
100
+ competing for CPU/disk/antivirus scanning at the same time.
101
+
102
+ Note that start_worker_pool() shares the same _starting bookkeeping,
103
+ so a pool worker that is still booting also counts against this cap.
104
+ """
105
+ if suspended:
106
+ return None, None
107
+ # 1. Look for an existing free worker. This also picks up workers that
108
+ # were started ahead of time by start_worker_pool(), whether or not
109
+ # they have finished booting yet.
110
+ for pid, w in list(_workers.items()):
111
+ if w["is_free"] and w["process"].is_alive():
112
+ logger.info(f"Reusing free worker {pid} (of {len(_workers)}) for request {list(data.keys())}")
113
+ w["is_free"] = False
114
+ w["request_queue"].put(data)
115
+ return w["result_queue"], pid
116
+
117
+ # 2. If no free worker was found, create a new one -- unless too many
118
+ # workers are already in the process of starting up.
119
+ if len(_starting) >= MAX_CONCURRENT_STARTING:
120
+ logger.info(
121
+ f"Declining to start a new worker for request "
122
+ f"{list(data.keys())}; {len(_starting)} worker(s) already "
123
+ "starting."
124
+ )
125
+ return None, None
126
+
127
+ pid = _start_new_worker(is_free=False)
128
+ w = _workers[pid]
129
+ logger.info(f"Sending request {data['action']} to newly created worker {pid}")
130
+ w["request_queue"].put(data)
131
+ return w["result_queue"], pid
132
+
133
+ def start_worker_pool():
134
+ """
135
+ Proactively start up to MAX_CONCURRENT_STARTING idle worker processes,
136
+ so that a free (or at least already-booting) worker is more likely to
137
+ be available by the time the user actually triggers a request -- e.g.
138
+ right when they start typing.
139
+
140
+ Workers started here are tracked in _starting exactly like workers
141
+ created on-demand by send_worker_request(), and are marked as free
142
+ immediately, so send_worker_request() will pick them up as soon as
143
+ they exist -- whether or not they've finished booting yet.
144
+
145
+ Intended to be called at strategic moments (e.g. app startup, after
146
+ resume(), or when a new editor is opened), not from a tight loop: each
147
+ call may create up to MAX_CONCURRENT_STARTING new processes on top of
148
+ whatever is already starting.
149
+ """
150
+ if suspended:
151
+ logger.info("Not starting worker pool because workers are suspended.")
152
+ return
153
+ for _ in range(MAX_CONCURRENT_STARTING):
154
+ _start_new_worker(is_free=True)
155
+ logger.info(f"Started worker pool of {MAX_CONCURRENT_STARTING} worker(s).")
84
156
 
85
157
  def mark_worker_as_free(pid: int):
86
158
  """
87
159
  Mark a previously-used worker process (identified by pid)
88
160
  as free for reuse. If the worker has died, clean it up instead.
161
+
162
+ This is also the point at which a worker is considered to have proven
163
+ itself alive-and-useful (it has completed a request), so we release
164
+ its "starting" slot here if it still held one -- whether it succeeded
165
+ or died in the process.
89
166
  """
90
167
  w = _workers.get(pid)
91
168
  if w is None:
92
169
  return
93
170
  if w["process"].is_alive():
94
171
  w["is_free"] = True
172
+ _starting.discard(pid)
95
173
  logger.info(f"Marking worker {pid} as free")
96
174
  else:
97
175
  # The worker died while handling a request; clean up its resources
@@ -99,6 +177,7 @@ def mark_worker_as_free(pid: int):
99
177
  _workers.pop(pid)
100
178
  _close_worker_queues(w)
101
179
  w["process"].join(timeout=1)
180
+ _starting.discard(pid)
102
181
 
103
182
  def check_worker_alive(pid: int) -> bool:
104
183
  w = _workers.get(pid)
@@ -128,7 +207,7 @@ def reap_stopping_workers():
128
207
  logger.warning(f"Error joining stopped worker {pid}: {e}")
129
208
  _close_worker_queues(w)
130
209
 
131
- def stop_unused_workers(max_free: int = 1, force: bool = False):
210
+ def stop_unused_workers(max_free: int = 3, force: bool = False):
132
211
  """
133
212
  Request that free worker processes stop until there is at most
134
213
  'max_free' free processes left. This keeps us from accumulating too
@@ -143,6 +222,11 @@ def stop_unused_workers(max_free: int = 1, force: bool = False):
143
222
  joining a process that hasn't exited yet can block for a noticeable
144
223
  amount of time, especially on Windows, and this function is typically
145
224
  called from a periodic timer on the main/GUI thread.
225
+
226
+ Note: if max_free is set lower than MAX_CONCURRENT_STARTING, workers
227
+ just started by start_worker_pool() may be asked to stop again before
228
+ they ever get used. The default of 3 leaves headroom above
229
+ MAX_CONCURRENT_STARTING (2).
146
230
  """
147
231
  global _last_stop_unused_time
148
232
  if not force and time.time() - _last_stop_unused_time < STOP_UNUSED_INTERVAL:
@@ -197,6 +281,8 @@ def stop_all_workers():
197
281
  logger.warning(f"Error joining stopping worker {pid}: {e}")
198
282
  _close_worker_queues(w)
199
283
  del _stopping[pid]
284
+ # Nothing is left running, so no worker can still be "starting".
285
+ _starting.clear()
200
286
  logger.info("All workers stopped.")
201
287
 
202
288