easycoder 251215.2__py2.py3-none-any.whl → 260111.1__py2.py3-none-any.whl

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 (48) hide show
  1. easycoder/__init__.py +4 -3
  2. easycoder/debugger/ec_dbg_value_display copy.py +1 -1
  3. easycoder/debugger/ec_dbg_value_display.py +12 -11
  4. easycoder/debugger/ec_dbg_watchlist.py +146 -12
  5. easycoder/debugger/ec_debug.py +85 -8
  6. easycoder/ec_classes.py +228 -25
  7. easycoder/ec_compiler.py +29 -8
  8. easycoder/ec_core.py +364 -242
  9. easycoder/ec_gclasses.py +20 -9
  10. easycoder/ec_graphics.py +42 -26
  11. easycoder/ec_handler.py +4 -3
  12. easycoder/ec_mqtt.py +248 -0
  13. easycoder/ec_program.py +63 -28
  14. easycoder/ec_psutil.py +1 -1
  15. easycoder/ec_value.py +57 -36
  16. easycoder/pre/README.md +3 -0
  17. easycoder/pre/__init__.py +17 -0
  18. easycoder/pre/debugger/__init__.py +5 -0
  19. easycoder/pre/debugger/ec_dbg_value_display copy.py +195 -0
  20. easycoder/pre/debugger/ec_dbg_value_display.py +24 -0
  21. easycoder/pre/debugger/ec_dbg_watch_list copy.py +219 -0
  22. easycoder/pre/debugger/ec_dbg_watchlist.py +293 -0
  23. easycoder/pre/debugger/ec_debug.py +1014 -0
  24. easycoder/pre/ec_border.py +67 -0
  25. easycoder/pre/ec_classes.py +470 -0
  26. easycoder/pre/ec_compiler.py +291 -0
  27. easycoder/pre/ec_condition.py +27 -0
  28. easycoder/pre/ec_core.py +2772 -0
  29. easycoder/pre/ec_gclasses.py +230 -0
  30. easycoder/pre/ec_graphics.py +1682 -0
  31. easycoder/pre/ec_handler.py +79 -0
  32. easycoder/pre/ec_keyboard.py +439 -0
  33. easycoder/pre/ec_program.py +557 -0
  34. easycoder/pre/ec_psutil.py +48 -0
  35. easycoder/pre/ec_timestamp.py +11 -0
  36. easycoder/pre/ec_value.py +124 -0
  37. easycoder/pre/icons/close.png +0 -0
  38. easycoder/pre/icons/exit.png +0 -0
  39. easycoder/pre/icons/run.png +0 -0
  40. easycoder/pre/icons/step.png +0 -0
  41. easycoder/pre/icons/stop.png +0 -0
  42. easycoder/pre/icons/tick.png +0 -0
  43. {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/METADATA +1 -1
  44. easycoder-260111.1.dist-info/RECORD +59 -0
  45. easycoder-251215.2.dist-info/RECORD +0 -31
  46. {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/WHEEL +0 -0
  47. {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/entry_points.txt +0 -0
  48. {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/licenses/LICENSE +0 -0
easycoder/__init__.py CHANGED
@@ -1,17 +1,18 @@
1
1
  '''EasyCoder for Python'''
2
2
 
3
+ from .ec_border import *
3
4
  from .ec_classes import *
4
- from .ec_gclasses import *
5
5
  from .ec_compiler import *
6
6
  from .ec_condition import *
7
7
  from .ec_core import *
8
+ from .ec_gclasses import *
8
9
  from .ec_graphics import *
9
10
  from .ec_handler import *
10
11
  from .ec_keyboard import *
11
- from .ec_border import *
12
+ from .ec_mqtt import *
12
13
  from .ec_program import *
13
14
  from .ec_psutil import *
14
15
  from .ec_timestamp import *
15
16
  from .ec_value import *
16
17
 
17
- __version__ = "251215.2"
18
+ __version__ = "260111.1"
@@ -122,7 +122,7 @@ class ValueDisplay(QWidget):
122
122
  else:
123
123
  val_type = val.get('type', '?')
124
124
  content = val.get('content', '')
125
- if val_type == 'str':
125
+ if val_type == str:
126
126
  # keep each element concise
127
127
  s = str(content)
128
128
  if len(s) > 120:
@@ -1,13 +1,6 @@
1
1
  """ValueDisplay widget for displaying variable values in the EasyCoder debugger"""
2
2
 
3
- from PySide6.QtWidgets import (
4
- QWidget,
5
- QFrame,
6
- QVBoxLayout,
7
- QLabel,
8
- QScrollArea,
9
- )
10
- from PySide6.QtCore import Qt
3
+ from PySide6.QtWidgets import QLabel
11
4
 
12
5
 
13
6
  class ValueDisplay(QLabel):
@@ -15,9 +8,17 @@ class ValueDisplay(QLabel):
15
8
 
16
9
  def __init__(self, parent=None):
17
10
  super().__init__(parent)
18
-
19
- def setValue(self, program, symbol_name):
11
+
12
+ @staticmethod
13
+ def render_text(program, symbol_name):
20
14
  record = program.getVariable(symbol_name)
21
15
  value = program.textify(record)
22
- self.setText(str(value))
16
+ return None if value is None else str(value)
17
+
18
+ def setValue(self, program, symbol_name):
19
+ try:
20
+ rendered = self.render_text(program, symbol_name)
21
+ except Exception as exc:
22
+ rendered = f"<error: {exc}>"
23
+ self.setText(rendered if rendered is not None else "")
23
24
 
@@ -1,6 +1,7 @@
1
1
  """WatchListWidget showing variables on a single line with scrollable values."""
2
2
 
3
3
  import bisect
4
+ import json
4
5
 
5
6
  from PySide6.QtWidgets import (
6
7
  QWidget,
@@ -10,8 +11,12 @@ from PySide6.QtWidgets import (
10
11
  QPushButton,
11
12
  QSizePolicy,
12
13
  QScrollArea,
14
+ QPlainTextEdit,
15
+ QFrame,
16
+ QSplitter,
13
17
  )
14
18
  from PySide6.QtCore import Qt
19
+ from easycoder.ec_classes import ECVariable, ECDictionary, ECList, ECValue
15
20
  from .ec_dbg_value_display import ValueDisplay
16
21
 
17
22
 
@@ -25,9 +30,20 @@ class WatchListWidget(QWidget):
25
30
  self._variable_set: set[str] = set()
26
31
  self._order: list[str] = []
27
32
  self._placeholder: QLabel | None = None
33
+ self._expanded_name: str | None = None
28
34
 
35
+ main_layout = QVBoxLayout(self)
36
+ main_layout.setContentsMargins(0, 0, 0, 0)
37
+ main_layout.setSpacing(6)
38
+
39
+ self.splitter = QSplitter(Qt.Orientation.Vertical, self)
40
+ self.splitter.setHandleWidth(6)
41
+ self.splitter.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
42
+
43
+ list_container = QWidget(self)
44
+ list_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
29
45
  # Outer layout: scrollable labels (left) and fixed button column (right)
30
- outer = QHBoxLayout(self)
46
+ outer = QHBoxLayout(list_container)
31
47
  outer.setContentsMargins(0, 0, 0, 0)
32
48
  outer.setSpacing(2)
33
49
 
@@ -54,8 +70,42 @@ class WatchListWidget(QWidget):
54
70
  self.buttons_column.addStretch(1)
55
71
  outer.addLayout(self.buttons_column)
56
72
 
73
+ self.splitter.addWidget(list_container)
74
+
75
+ self._build_expanded_panel()
76
+
77
+ main_layout.addWidget(self.splitter, 1)
78
+ # Give the list more space by default
79
+ try:
80
+ self.splitter.setStretchFactor(0, 3)
81
+ self.splitter.setStretchFactor(1, 2)
82
+ except Exception:
83
+ pass
84
+
57
85
  self._show_placeholder()
58
86
 
87
+ def _build_expanded_panel(self):
88
+ self.expanded_frame = QFrame(self)
89
+ self.expanded_frame.setFrameShape(QFrame.Shape.StyledPanel)
90
+ self.expanded_frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
91
+
92
+ expanded_layout = QVBoxLayout(self.expanded_frame)
93
+ expanded_layout.setContentsMargins(6, 6, 6, 6)
94
+ expanded_layout.setSpacing(4)
95
+
96
+ self.expanded_title = QLabel("Expanded view")
97
+ self.expanded_title.setStyleSheet("font-weight: bold;")
98
+ expanded_layout.addWidget(self.expanded_title)
99
+
100
+ self.expanded_body = QPlainTextEdit(self.expanded_frame)
101
+ self.expanded_body.setReadOnly(True)
102
+ self.expanded_body.setLineWrapMode(QPlainTextEdit.LineWrapMode.WidgetWidth)
103
+ self.expanded_body.setStyleSheet("font-family: monospace; background-color: #fafafa;")
104
+ expanded_layout.addWidget(self.expanded_body)
105
+
106
+ self.splitter.addWidget(self.expanded_frame)
107
+ self._clear_expanded_panel()
108
+
59
109
  def _show_placeholder(self):
60
110
  if self._placeholder is not None:
61
111
  return
@@ -90,9 +140,18 @@ class WatchListWidget(QWidget):
90
140
  label.setWordWrap(False)
91
141
  row_layout.addWidget(label, 1)
92
142
 
93
- # Remove button in separate column
94
- remove_btn = QPushButton("-")
143
+ # Buttons live in a side container: remove (x) and expand (+)
144
+ button_container = QWidget(self)
145
+ button_row = QHBoxLayout(button_container)
146
+ button_row.setContentsMargins(0, 0, 0, 0)
147
+ button_row.setSpacing(4)
148
+
149
+ remove_btn = QPushButton("x")
95
150
  remove_btn.setFixedWidth(22)
151
+ expand_btn = QPushButton("+")
152
+ expand_btn.setFixedWidth(22)
153
+ button_row.addWidget(remove_btn)
154
+ button_row.addWidget(expand_btn)
96
155
 
97
156
  def on_remove():
98
157
  try:
@@ -104,33 +163,43 @@ class WatchListWidget(QWidget):
104
163
  self._order.remove(name)
105
164
  self.content_layout.removeWidget(row_widget)
106
165
  row_widget.deleteLater()
107
- self.buttons_column.removeWidget(remove_btn)
108
- remove_btn.deleteLater()
166
+ self.buttons_column.removeWidget(button_container)
167
+ button_container.deleteLater()
109
168
  self._rows.pop(name, None)
169
+ if self._expanded_name == name:
170
+ self._clear_expanded_panel()
110
171
  if not self._rows:
172
+ self._clear_expanded_panel()
111
173
  self._show_placeholder()
112
174
  except Exception:
113
175
  pass
114
176
 
115
177
  remove_btn.clicked.connect(on_remove)
178
+ expand_btn.clicked.connect(lambda: self._on_expand(name))
116
179
 
117
180
  insert_pos = bisect.bisect_left(self._order, name)
118
181
  self._order.insert(insert_pos, name)
119
182
  # Insert label row above stretch, keeping alphabetical order
120
183
  self.content_layout.insertWidget(insert_pos, row_widget)
121
184
  # Insert button above stretch on the right, same position
122
- self.buttons_column.insertWidget(insert_pos, remove_btn)
185
+ self.buttons_column.insertWidget(insert_pos, button_container)
123
186
 
124
187
  # Align button height to row height
125
188
  row_widget.adjustSize()
126
189
  btn_h = row_widget.sizeHint().height()
127
190
  if btn_h > 0:
191
+ button_container.setFixedHeight(btn_h)
128
192
  remove_btn.setFixedHeight(btn_h)
193
+ expand_btn.setFixedHeight(btn_h)
129
194
 
130
195
  self._rows[name] = {
131
196
  'widget': row_widget,
132
197
  'label': label,
133
- 'button': remove_btn,
198
+ 'buttons': button_container,
199
+ 'remove_btn': remove_btn,
200
+ 'expand_btn': expand_btn,
201
+ 'summary': '',
202
+ 'detail': '',
134
203
  }
135
204
  self._variable_set.add(name)
136
205
 
@@ -143,13 +212,18 @@ class WatchListWidget(QWidget):
143
212
  row = self._rows.get(name)
144
213
  if not row:
145
214
  return
215
+ detail_text = ''
146
216
  try:
147
- val_display = ValueDisplay()
148
- val_display.setValue(program, name)
149
- value_text = val_display.text()
217
+ summary_text, detail_text = self._get_value_texts(program, name)
218
+ row['summary'] = summary_text
219
+ row['detail'] = detail_text
150
220
  except Exception as e:
151
- value_text = f"<error: {e}>"
152
- row['label'].setText(f"{name} = {value_text}")
221
+ summary_text = f"<error: {e}>"
222
+ row['summary'] = summary_text
223
+ row['detail'] = ''
224
+ row['label'].setText(f"{name} = {summary_text}")
225
+ if self._expanded_name == name:
226
+ self._apply_expanded_content(name, summary_text, detail_text)
153
227
 
154
228
  def refreshVariables(self, program):
155
229
  if not self._rows:
@@ -157,3 +231,63 @@ class WatchListWidget(QWidget):
157
231
  return
158
232
  for name in list(self._rows.keys()):
159
233
  self._refresh_one(name, program)
234
+
235
+ def _get_value_texts(self, program, name: str):
236
+ summary = ValueDisplay.render_text(program, name)
237
+ if summary is None:
238
+ summary = ''
239
+
240
+ try:
241
+ record = program.getVariable(name)
242
+ obj = program.getObject(record)
243
+ except Exception:
244
+ return summary, ''
245
+
246
+ detail = ''
247
+ try:
248
+ if isinstance(obj, ECVariable):
249
+ detail = summary
250
+ elif isinstance(obj, ECDictionary):
251
+ raw = obj.getValue()
252
+ if isinstance(raw, ECValue):
253
+ raw = raw.getContent()
254
+ if isinstance(raw, dict):
255
+ detail = json.dumps(raw, indent=2)
256
+ elif isinstance(obj, ECList):
257
+ raw = obj.getValue()
258
+ if isinstance(raw, ECValue):
259
+ raw = raw.getContent()
260
+ if isinstance(raw, list):
261
+ detail = json.dumps(raw, indent=2)
262
+ except Exception:
263
+ pass
264
+ return summary, detail
265
+
266
+ def _on_expand(self, name: str):
267
+ if name not in self._rows:
268
+ return
269
+ try:
270
+ if hasattr(self.debugger, 'program'):
271
+ self._refresh_one(name, self.debugger.program)
272
+ except Exception:
273
+ pass
274
+
275
+ row = self._rows.get(name)
276
+ if not row:
277
+ return
278
+ self._expanded_name = name
279
+ self._apply_expanded_content(name, row.get('summary', ''), row.get('detail', ''))
280
+
281
+ def _apply_expanded_content(self, name: str, summary: str, detail: str):
282
+ body = detail if detail else summary
283
+ if body is None:
284
+ body = ''
285
+ if not body:
286
+ body = 'No content available for this variable.'
287
+ self.expanded_title.setText(f"Expanded view: {name}")
288
+ self.expanded_body.setPlainText(body)
289
+
290
+ def _clear_expanded_panel(self):
291
+ self._expanded_name = None
292
+ self.expanded_title.setText("Expanded view")
293
+ self.expanded_body.setPlainText("Select a watched variable and click + to view its contents.")
@@ -43,6 +43,14 @@ class Debugger(QMainWindow):
43
43
  if not text:
44
44
  return
45
45
 
46
+ # Echo all output to original stdout with proper line breaks
47
+ try:
48
+ if self.debugger._orig_stdout:
49
+ self.debugger._orig_stdout.write(text)
50
+ self.debugger._orig_stdout.flush()
51
+ except Exception:
52
+ pass
53
+
46
54
  # Check if this looks like an error message - if so, also write to original stderr
47
55
  if any(err_marker in text for err_marker in ['Error', 'Traceback', 'Exception']):
48
56
  try:
@@ -418,6 +426,9 @@ class Debugger(QMainWindow):
418
426
  self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
419
427
  self.stopped = True
420
428
  self.skip_next_breakpoint = False # Flag to skip breakpoint check on resume
429
+ self.saved_queue = [] # Save queue state when stopped to preserve forked threads
430
+ self._highlighted: set[int] = set()
431
+ self.step_from_line: int | None = None # Track source line when stepping
421
432
 
422
433
  # try to load saved geometry from ~/.ecdebug.conf
423
434
  cfg_path = os.path.join(os.path.expanduser("~"), ".ecdebug.conf")
@@ -467,9 +478,10 @@ class Debugger(QMainWindow):
467
478
  left.setFrameShape(QFrame.Shape.StyledPanel)
468
479
  left_layout = QVBoxLayout(left)
469
480
  left_layout.setContentsMargins(8, 8, 8, 8)
481
+ left_layout.setSpacing(0)
470
482
  self.leftColumn = self.MainLeftColumn(self)
471
- left_layout.addWidget(self.leftColumn)
472
- left_layout.addStretch()
483
+ self.leftColumn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
484
+ left_layout.addWidget(self.leftColumn, 1)
473
485
 
474
486
  # Right pane
475
487
  right = QFrame()
@@ -794,7 +806,7 @@ class Debugger(QMainWindow):
794
806
  ###########################################################################
795
807
  # Set the background color of one line of the script
796
808
  def setBackground(self, lino, color):
797
- # Set the background color of the given line
809
+ # Set the background color of the given line and track highlighted lines
798
810
  if lino < 0 or lino >= len(self.scriptLines):
799
811
  return
800
812
  lineSpec = self.scriptLines[lino]
@@ -803,8 +815,16 @@ class Debugger(QMainWindow):
803
815
  return
804
816
  if color == 'none':
805
817
  panel.setStyleSheet("")
818
+ self._highlighted.discard(lino)
806
819
  else:
807
820
  panel.setStyleSheet(f"background-color: {color};")
821
+ self._highlighted.add(lino)
822
+
823
+ def _clearHighlights(self):
824
+ # Remove highlighting from all previously highlighted lines
825
+ for lino in list(self._highlighted):
826
+ self.setBackground(lino, 'none')
827
+ self._highlighted.clear()
808
828
 
809
829
  ###########################################################################
810
830
  # Here before each instruction is run
@@ -832,22 +852,32 @@ class Debugger(QMainWindow):
832
852
  if is_first_command:
833
853
  should_halt = True
834
854
  self.stopped = True
855
+ self.step_from_line = None
835
856
  print(f"Program ready at line {lino + 1}")
836
857
  # If we're in stopped (step) mode, halt after each command
837
858
  elif self.stopped:
838
- should_halt = True
859
+ # If stepping, only halt when we reach a different source line
860
+ if self.step_from_line is not None:
861
+ if lino != self.step_from_line:
862
+ should_halt = True
863
+ self.step_from_line = None
864
+ else:
865
+ should_halt = True
839
866
  # If there's a breakpoint on this line, halt
840
867
  elif bp:
841
868
  print(f"Hit breakpoint at line {lino + 1}")
842
869
  self.stopped = True
843
870
  should_halt = True
844
871
 
845
- # If halting, update the UI
872
+ # If halting, update the UI and save queue state
846
873
  if should_halt:
847
874
  self.scrollTo(lino)
848
- self.setBackground(lino, 'LightYellow')
875
+ self._clearHighlights()
876
+ self.setBackground(lino, 'Yellow')
849
877
  # Refresh variable values when halted
850
878
  self.refreshVariables()
879
+ # Save the current queue state to preserve forked threads
880
+ self._saveQueueState()
851
881
 
852
882
  return should_halt
853
883
 
@@ -859,6 +889,28 @@ class Debugger(QMainWindow):
859
889
  except Exception as ex:
860
890
  print(f"Error refreshing variables: {ex}")
861
891
 
892
+ def _saveQueueState(self):
893
+ """Save the current global queue state (preserves forked threads)"""
894
+ try:
895
+ # Import the module to access the global queue
896
+ from easycoder import ec_program
897
+ # Save a copy of the queue
898
+ self.saved_queue = list(ec_program.queue)
899
+ except Exception as ex:
900
+ print(f"Error saving queue state: {ex}")
901
+
902
+ def _restoreQueueState(self):
903
+ """Restore the saved queue state (resume all forked threads)"""
904
+ try:
905
+ # Import here to avoid circular dependency
906
+ from easycoder import ec_program
907
+ # Restore the queue from saved state
908
+ if self.saved_queue:
909
+ ec_program.queue.clear()
910
+ ec_program.queue.extend(self.saved_queue)
911
+ except Exception as ex:
912
+ print(f"Error restoring queue state: {ex}")
913
+
862
914
  def doRun(self):
863
915
  """Resume free-running execution from current PC"""
864
916
  command = self.program.code[self.pc]
@@ -870,12 +922,18 @@ class Debugger(QMainWindow):
870
922
 
871
923
  # Switch to free-running mode
872
924
  self.stopped = False
925
+ self.step_from_line = None
873
926
 
874
927
  # Skip the breakpoint check for the current instruction (the one we're resuming from)
875
928
  self.skip_next_breakpoint = True
876
929
 
877
- # Resume execution at current PC
930
+ # Restore the saved queue state to resume all forked threads
931
+ self._restoreQueueState()
932
+
933
+ # Enqueue the current thread, then flush immediately
878
934
  self.program.run(self.pc)
935
+ from easycoder.ec_program import flush
936
+ flush()
879
937
 
880
938
  def doStep(self):
881
939
  """Execute one instruction and halt again"""
@@ -887,14 +945,33 @@ class Debugger(QMainWindow):
887
945
 
888
946
  # Stay in stopped mode (will halt after next instruction)
889
947
  self.stopped = True
948
+ # Remember the line we're stepping from - don't halt until we reach a different line
949
+ self.step_from_line = lino
890
950
 
891
951
  # Skip the breakpoint check for the current instruction (the one we're stepping from)
892
952
  self.skip_next_breakpoint = True
893
953
 
894
- # Execute the current instruction
954
+ # Restore the saved queue state to resume all forked threads
955
+ self._restoreQueueState()
956
+
957
+ # Enqueue the current thread, then flush a single cycle
895
958
  self.program.run(self.pc)
959
+ from easycoder.ec_program import flush
960
+ flush()
896
961
 
897
962
  def doStop(self):
963
+ try:
964
+ lino = self.program.code[self.pc].get('lino', 0) + 1
965
+ print(f"Stopped by user at line {lino}")
966
+ except Exception:
967
+ print("Stopped by user")
968
+ # Clear all previous highlights and mark the current line
969
+ try:
970
+ self._clearHighlights()
971
+ current_lino = self.program.code[self.pc].get('lino', 0)
972
+ self.setBackground(current_lino, 'LightYellow')
973
+ except Exception:
974
+ pass
898
975
  self.stopped = True
899
976
 
900
977
  def doClose(self):