PrEditor 2.1.0__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 (179) hide show
  1. preditor/__init__.py +315 -0
  2. preditor/__main__.py +13 -0
  3. preditor/about_module.py +165 -0
  4. preditor/cli.py +192 -0
  5. preditor/config.py +318 -0
  6. preditor/constants.py +13 -0
  7. preditor/contexts.py +210 -0
  8. preditor/cores/__init__.py +0 -0
  9. preditor/cores/core.py +20 -0
  10. preditor/dccs/.hab.json +10 -0
  11. preditor/dccs/maya/PrEditor_maya.mod +1 -0
  12. preditor/dccs/maya/README.md +22 -0
  13. preditor/dccs/maya/plug-ins/PrEditor_maya.py +141 -0
  14. preditor/dccs/studiomax/PackageContents.xml +32 -0
  15. preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr +8 -0
  16. preditor/dccs/studiomax/README.md +17 -0
  17. preditor/dccs/studiomax/preditor.ms +16 -0
  18. preditor/dccs/studiomax/preditor_menu.mnx +7 -0
  19. preditor/debug.py +149 -0
  20. preditor/delayable_engine/__init__.py +302 -0
  21. preditor/delayable_engine/delayables.py +85 -0
  22. preditor/enum.py +728 -0
  23. preditor/excepthooks.py +165 -0
  24. preditor/gui/__init__.py +56 -0
  25. preditor/gui/app.py +163 -0
  26. preditor/gui/codehighlighter.py +289 -0
  27. preditor/gui/completer.py +237 -0
  28. preditor/gui/console.py +605 -0
  29. preditor/gui/console_base.py +911 -0
  30. preditor/gui/dialog.py +181 -0
  31. preditor/gui/drag_tab_bar.py +625 -0
  32. preditor/gui/editor_chooser.py +57 -0
  33. preditor/gui/errordialog.py +69 -0
  34. preditor/gui/find_files.py +137 -0
  35. preditor/gui/fuzzy_search/__init__.py +0 -0
  36. preditor/gui/fuzzy_search/fuzzy_search.py +97 -0
  37. preditor/gui/group_tab_widget/__init__.py +0 -0
  38. preditor/gui/group_tab_widget/group_tab_widget.py +528 -0
  39. preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
  40. preditor/gui/group_tab_widget/grouped_tab_models.py +107 -0
  41. preditor/gui/group_tab_widget/grouped_tab_widget.py +223 -0
  42. preditor/gui/group_tab_widget/one_tab_widget.py +96 -0
  43. preditor/gui/level_buttons.py +358 -0
  44. preditor/gui/logger_window_handler.py +77 -0
  45. preditor/gui/logger_window_plugin.py +35 -0
  46. preditor/gui/loggerwindow.py +2405 -0
  47. preditor/gui/newtabwidget.py +69 -0
  48. preditor/gui/output_console.py +11 -0
  49. preditor/gui/qtdesigner/__init__.py +21 -0
  50. preditor/gui/qtdesigner/_log_plugin.py +29 -0
  51. preditor/gui/qtdesigner/console_base_plugin.py +48 -0
  52. preditor/gui/qtdesigner/console_predit_plugin.py +48 -0
  53. preditor/gui/set_text_editor_path_dialog.py +61 -0
  54. preditor/gui/status_label.py +99 -0
  55. preditor/gui/suggest_path_quotes_dialog.py +50 -0
  56. preditor/gui/ui/editor_chooser.ui +93 -0
  57. preditor/gui/ui/errordialog.ui +74 -0
  58. preditor/gui/ui/find_files.ui +140 -0
  59. preditor/gui/ui/loggerwindow.ui +1909 -0
  60. preditor/gui/ui/set_text_editor_path_dialog.ui +189 -0
  61. preditor/gui/ui/suggest_path_quotes_dialog.ui +225 -0
  62. preditor/gui/window.py +161 -0
  63. preditor/gui/workbox_mixin.py +1139 -0
  64. preditor/gui/workbox_text_edit.py +136 -0
  65. preditor/gui/workboxwidget.py +315 -0
  66. preditor/logging_config.py +55 -0
  67. preditor/osystem.py +401 -0
  68. preditor/plugins.py +118 -0
  69. preditor/prefs.py +381 -0
  70. preditor/resource/environment_variables.html +26 -0
  71. preditor/resource/error_mail.html +85 -0
  72. preditor/resource/error_mail_inline.html +41 -0
  73. preditor/resource/img/README.md +17 -0
  74. preditor/resource/img/arrow_forward.png +0 -0
  75. preditor/resource/img/check-bold.png +0 -0
  76. preditor/resource/img/chevron-down.png +0 -0
  77. preditor/resource/img/chevron-up.png +0 -0
  78. preditor/resource/img/close-thick.png +0 -0
  79. preditor/resource/img/comment-edit.png +0 -0
  80. preditor/resource/img/content-copy.png +0 -0
  81. preditor/resource/img/content-cut.png +0 -0
  82. preditor/resource/img/content-duplicate.png +0 -0
  83. preditor/resource/img/content-paste.png +0 -0
  84. preditor/resource/img/content-save.png +0 -0
  85. preditor/resource/img/debug_disabled.png +0 -0
  86. preditor/resource/img/eye-check.png +0 -0
  87. preditor/resource/img/file-plus.png +0 -0
  88. preditor/resource/img/file-remove.png +0 -0
  89. preditor/resource/img/format-align-left.png +0 -0
  90. preditor/resource/img/format-letter-case-lower.png +0 -0
  91. preditor/resource/img/format-letter-case-upper.png +0 -0
  92. preditor/resource/img/format-letter-case.svg +1 -0
  93. preditor/resource/img/information.png +0 -0
  94. preditor/resource/img/logging_critical.png +0 -0
  95. preditor/resource/img/logging_custom.png +0 -0
  96. preditor/resource/img/logging_debug.png +0 -0
  97. preditor/resource/img/logging_error.png +0 -0
  98. preditor/resource/img/logging_info.png +0 -0
  99. preditor/resource/img/logging_not_set.png +0 -0
  100. preditor/resource/img/logging_warning.png +0 -0
  101. preditor/resource/img/marker.png +0 -0
  102. preditor/resource/img/play.png +0 -0
  103. preditor/resource/img/playlist-play.png +0 -0
  104. preditor/resource/img/plus-minus-variant.png +0 -0
  105. preditor/resource/img/preditor.ico +0 -0
  106. preditor/resource/img/preditor.png +0 -0
  107. preditor/resource/img/preditor.psd +0 -0
  108. preditor/resource/img/preditor.svg +44 -0
  109. preditor/resource/img/regex.svg +1 -0
  110. preditor/resource/img/restart.svg +1 -0
  111. preditor/resource/img/skip-forward-outline.png +0 -0
  112. preditor/resource/img/skip-next-outline.png +0 -0
  113. preditor/resource/img/skip-next.png +0 -0
  114. preditor/resource/img/skip-previous.png +0 -0
  115. preditor/resource/img/subdirectory-arrow-right.png +0 -0
  116. preditor/resource/img/text-search-variant.png +0 -0
  117. preditor/resource/img/warning-big.png +0 -0
  118. preditor/resource/lang/python.json +30 -0
  119. preditor/resource/pref_updates/pref_updates.json +17 -0
  120. preditor/resource/settings.ini +25 -0
  121. preditor/resource/stylesheet/Bright.css +76 -0
  122. preditor/resource/stylesheet/Dark.css +210 -0
  123. preditor/scintilla/__init__.py +40 -0
  124. preditor/scintilla/delayables/__init__.py +11 -0
  125. preditor/scintilla/delayables/smart_highlight.py +97 -0
  126. preditor/scintilla/delayables/spell_check.py +174 -0
  127. preditor/scintilla/documenteditor.py +1924 -0
  128. preditor/scintilla/finddialog.py +68 -0
  129. preditor/scintilla/lang/__init__.py +80 -0
  130. preditor/scintilla/lang/config/bash.ini +15 -0
  131. preditor/scintilla/lang/config/batch.ini +14 -0
  132. preditor/scintilla/lang/config/cpp.ini +19 -0
  133. preditor/scintilla/lang/config/css.ini +19 -0
  134. preditor/scintilla/lang/config/eyeonscript.ini +17 -0
  135. preditor/scintilla/lang/config/html.ini +21 -0
  136. preditor/scintilla/lang/config/javascript.ini +24 -0
  137. preditor/scintilla/lang/config/lua.ini +16 -0
  138. preditor/scintilla/lang/config/maxscript.ini +20 -0
  139. preditor/scintilla/lang/config/mel.ini +18 -0
  140. preditor/scintilla/lang/config/mu.ini +22 -0
  141. preditor/scintilla/lang/config/nsi.ini +19 -0
  142. preditor/scintilla/lang/config/perl.ini +19 -0
  143. preditor/scintilla/lang/config/puppet.ini +19 -0
  144. preditor/scintilla/lang/config/python.ini +28 -0
  145. preditor/scintilla/lang/config/ruby.ini +19 -0
  146. preditor/scintilla/lang/config/sql.ini +7 -0
  147. preditor/scintilla/lang/config/xml.ini +21 -0
  148. preditor/scintilla/lang/config/yaml.ini +18 -0
  149. preditor/scintilla/lang/language.py +240 -0
  150. preditor/scintilla/lexers/__init__.py +0 -0
  151. preditor/scintilla/lexers/cpplexer.py +22 -0
  152. preditor/scintilla/lexers/javascriptlexer.py +27 -0
  153. preditor/scintilla/lexers/maxscriptlexer.py +235 -0
  154. preditor/scintilla/lexers/mellexer.py +369 -0
  155. preditor/scintilla/lexers/mulexer.py +33 -0
  156. preditor/scintilla/lexers/pythonlexer.py +42 -0
  157. preditor/scintilla/ui/finddialog.ui +160 -0
  158. preditor/settings.py +71 -0
  159. preditor/stream/__init__.py +72 -0
  160. preditor/stream/console_handler.py +169 -0
  161. preditor/stream/director.py +144 -0
  162. preditor/stream/manager.py +97 -0
  163. preditor/streamhandler_helper.py +46 -0
  164. preditor/utils/__init__.py +191 -0
  165. preditor/utils/call_stack.py +86 -0
  166. preditor/utils/cute.py +106 -0
  167. preditor/utils/stylesheets.py +54 -0
  168. preditor/utils/text_search.py +338 -0
  169. preditor/version.py +34 -0
  170. preditor/weakref.py +363 -0
  171. preditor-2.1.0.dist-info/METADATA +308 -0
  172. preditor-2.1.0.dist-info/RECORD +179 -0
  173. preditor-2.1.0.dist-info/WHEEL +5 -0
  174. preditor-2.1.0.dist-info/entry_points.txt +19 -0
  175. preditor-2.1.0.dist-info/licenses/LICENSE +165 -0
  176. preditor-2.1.0.dist-info/top_level.txt +3 -0
  177. tests/encodings/test_ecoding.py +33 -0
  178. tests/find_files/test_find_files.py +74 -0
  179. tests/ide/test_delayable_engine.py +171 -0
@@ -0,0 +1,911 @@
1
+ import os
2
+ import re
3
+ import string
4
+ import subprocess
5
+ import time
6
+ import traceback
7
+ from fractions import Fraction
8
+ from typing import Optional
9
+
10
+ from Qt import QtCompat
11
+ from Qt.QtCore import Property, Qt
12
+ from Qt.QtGui import (
13
+ QColor,
14
+ QFontMetrics,
15
+ QIcon,
16
+ QKeySequence,
17
+ QTextCharFormat,
18
+ QTextCursor,
19
+ )
20
+ from Qt.QtWidgets import QAction, QApplication, QTextEdit, QWidget
21
+
22
+ from .. import instance, resourcePath, stream
23
+ from ..constants import StreamType
24
+ from ..stream.console_handler import FormatterDescriptor, HandlerInfo
25
+ from ..utils.cute import QtPropertyInit
26
+ from .codehighlighter import CodeHighlighter
27
+ from .loggerwindow import LoggerWindow
28
+ from .suggest_path_quotes_dialog import SuggestPathQuotesDialog
29
+
30
+
31
+ class ConsoleBase(QTextEdit):
32
+ """Base class for a text widget used to show stdout/stderr writes."""
33
+
34
+ _default_format = (
35
+ '%(levelname)s %(module)s.%(funcName)s line:%(lineno)d - %(message)s'
36
+ )
37
+ logging_formatter = FormatterDescriptor(default=_default_format)
38
+ """Used to format logging messages if logging_handlers doesn't define it."""
39
+
40
+ def __init__(self, parent: QWidget, controller: Optional[LoggerWindow] = None):
41
+ super().__init__(parent)
42
+ self._controller = None
43
+ self._stylesheet_changed_meta = None
44
+ self.controller = controller
45
+
46
+ self._first_show = True
47
+ # The last time a repaint call was made when writing. Used to limit the
48
+ # number of refreshes to once per X.X seconds.
49
+ self._last_repaint_time = 0
50
+ # The last time `QApplication.processEvents()` was called while writing.
51
+ # Optionally used to prevent the app from being marked as not responding
52
+ # while processing blocking code and ensure _last_repaint_time processes.
53
+ self._last_process_events_time = 0
54
+ self._write_error_self_destruct = False
55
+ """If enabling tracebacks using `OverrideConsoleStreams`, this is set to True
56
+ so only if write_error is called by sys.excepthook it will remove itself.
57
+ """
58
+
59
+ # Create the highlighter
60
+ highlight = CodeHighlighter(self, 'Python')
61
+ self.setCodeHighlighter(highlight)
62
+
63
+ self.addSepNewline = False
64
+ self.consoleLine = None
65
+ self.mousePressPos = None
66
+ self.logging_info = {}
67
+
68
+ self.init_actions()
69
+
70
+ def __repr__(self):
71
+ """The repr for this object including its objectName if set."""
72
+ name = self.objectName()
73
+ if name:
74
+ name = f" named {name!r}"
75
+ module = type(self).__module__
76
+ class_ = type(self).__name__
77
+
78
+ return f"<{module}.{class_}{name} object at 0x{id(self):016X}>"
79
+
80
+ @classmethod
81
+ def __defineRegexPatterns(cls):
82
+ """Define various regex patterns to use to determine if the msg to write
83
+ is part of a traceback, and also to determine if it is from a workbox, or
84
+ from the console.
85
+
86
+ We construct various parts of the patterns, and combine them into the
87
+ final patterns. The workbox pattern and console pattern share the line
88
+ and in_str parts.
89
+ """
90
+
91
+ # For Traceback workbox lines, use this regex pattern, so we can extract
92
+ # workboxName and lineNum. Note that Syntax errors present slightly
93
+ # differently than other Exceptions.
94
+ # SyntaxErrors:
95
+ # - Do NOT include the text ", in" followed by a module
96
+ # - DO include the offending line of code
97
+ # Other Exceptions
98
+ # - DO include the text ", in" followed by a module
99
+ # - Do NOT include the offending line of code if from stdIn (ie
100
+ # a workbox)
101
+ # So we will use the presence of the text ", in" to tell use whether to
102
+ # fake the offending code line or not.
103
+
104
+ # Define pattern pieces
105
+ console_pattern = r'File "<ConsolePrEdit>", '
106
+ workbox_pattern = r'File "<Workbox(?:Selection)?>:(?P<workboxName>.*)", '
107
+ line_pattern = r'line (?P<lineNum>\d{1,6})'
108
+ in_str_pattern = r'(?P<inStr>, in)?'
109
+
110
+ # Put the pattern pieces to together to make patterns
111
+ workbox_pattern = workbox_pattern + line_pattern + in_str_pattern
112
+ cls.workbox_pattern = re.compile(workbox_pattern)
113
+
114
+ console_pattern = console_pattern + line_pattern + in_str_pattern
115
+ cls.console_pattern = re.compile(console_pattern)
116
+
117
+ # Define a pattern to capture info from tracebacks. The newline/$ section
118
+ # handle SyntaxError output that does not include the `, in ...` portion.
119
+ pattern = r'File "(?P<filename>.*)", line (?P<lineNum>\d{1,10})(, in|\r\n|\n|$)'
120
+ cls.traceback_pattern = re.compile(pattern)
121
+
122
+ def add_separator(self):
123
+ """Add a marker line for visual separation of console output."""
124
+ # Ensure the input is written to the end of the document on a new line
125
+ self.startPrompt("")
126
+ # Add a horizontal rule
127
+ self.insertHtml("<hr><br>")
128
+
129
+ def clear(self):
130
+ """clears the text in the editor"""
131
+ super().clear()
132
+ # Ensure the console is refreshed in case the user is clearing the console
133
+ # as part of a blocking call.
134
+ self.maybeRepaint(force=True)
135
+
136
+ def contextMenuEvent(self, event):
137
+ """Builds a custom right click menu to show."""
138
+ # Create the standard menu and allow subclasses to customize it
139
+ menu = self.createStandardContextMenu(event.pos())
140
+ menu = self.update_context_menu(menu)
141
+ if self.controller:
142
+ menu.setFont(self.controller.font())
143
+ menu.exec(self.mapToGlobal(event.pos()))
144
+
145
+ @property
146
+ def controller(self) -> Optional[LoggerWindow]:
147
+ """Used to access workbox widgets and PrEditor settings that are needed.
148
+
149
+ This must be set to a LoggerWindow instance. If not set then uses
150
+ `self.window()`. If this instance isn't a child of a LoggerWindow you must
151
+ set controller to an instance of a LoggerWindow.
152
+ """
153
+ if self._controller:
154
+ return self._controller
155
+ controller = self.window()
156
+ if not isinstance(controller, LoggerWindow):
157
+ controller = instance(create=False)
158
+ return controller
159
+
160
+ @controller.setter
161
+ def controller(self, value: LoggerWindow):
162
+ if self._stylesheet_changed_meta and self.controller:
163
+ # Remove the existing signals if connected
164
+ self.controller.styleSheetChanged.disconnect(self._stylesheet_changed_meta)
165
+ self._stylesheet_changed_meta = None
166
+
167
+ self._controller = value
168
+ # Ensure the stylesheet is up to date and stays up to date.
169
+ self.init_stylesheet()
170
+
171
+ def codeHighlighter(self):
172
+ """Get the code highlighter for the console
173
+
174
+ Returns:
175
+ _uiCodeHighlighter (CodeHighlighter): The instantiated CodeHighlighter
176
+ """
177
+ return self._uiCodeHighlighter
178
+
179
+ def setCodeHighlighter(self, highlight):
180
+ """Set the code highlighter for the console
181
+
182
+ Args:
183
+ highlight (CodeHighlighter): The instantiated CodeHighlighter
184
+ """
185
+ self._uiCodeHighlighter = highlight
186
+
187
+ def errorHyperlink(self, anchor):
188
+ """Determine if chosen line is an error traceback file-info line, if so, parse
189
+ the filepath and line number, and attempt to open the module file in the user's
190
+ chosen text editor at the relevant line, using specified Command Prompt pattern.
191
+
192
+ The text editor defaults to SublimeText3, in the normal install directory
193
+ """
194
+ if not self.controller:
195
+ # Bail if there isn't a controller
196
+ return
197
+ # Bail if Error Hyperlinks setting is not turned on or we don't have an anchor.
198
+ doHyperlink = (
199
+ self.controller
200
+ and self.controller.uiErrorHyperlinksCHK.isChecked()
201
+ and anchor
202
+ )
203
+ if not doHyperlink:
204
+ return
205
+
206
+ # info is a comma separated string, in the form: "filename, workboxIdx, lineNum"
207
+ info = anchor.split(', ')
208
+ modulePath = info[0]
209
+ workboxName = info[1]
210
+ lineNum = info[2]
211
+
212
+ # fetch info from LoggerWindow
213
+ exePath = self.controller.textEditorPath
214
+ cmdTempl = self.controller.textEditorCmdTempl
215
+
216
+ # Bail if not setup properly
217
+ if not workboxName:
218
+ msg = (
219
+ "Cannot use traceback hyperlink (Correct the path with Options "
220
+ "> Set Preferred Text Editor Path).\n"
221
+ )
222
+ if not exePath:
223
+ msg += "No text editor path defined."
224
+ print(msg)
225
+ return
226
+ if not os.path.exists(exePath):
227
+ msg += "Text editor executable does not exist: {}".format(exePath)
228
+ print(msg)
229
+ return
230
+ if not cmdTempl:
231
+ msg += "No text editor Command Prompt command template defined."
232
+ print(msg)
233
+ return
234
+ if modulePath and not os.path.exists(modulePath):
235
+ msg += "Specified module path does not exist: {}".format(modulePath)
236
+ print(msg)
237
+ return
238
+
239
+ if modulePath:
240
+ # Check if cmdTempl filepaths aren't wrapped in double=quotes to handle
241
+ # spaces. If not, suggest to user to update the template, offering the
242
+ # suggested change.
243
+ pattern = r"(?<!\")({\w+Path})(?!\")"
244
+ repl = r'"\g<1>"'
245
+ quotedCmdTempl = re.sub(pattern, repl, cmdTempl)
246
+ if quotedCmdTempl != cmdTempl:
247
+ # Instantiate dialog to maybe show (unless user previously chose "Don't
248
+ # ask again")
249
+ dialog = SuggestPathQuotesDialog(
250
+ self.controller, cmdTempl, quotedCmdTempl
251
+ )
252
+ self.controller.maybeDisplayDialog(dialog)
253
+
254
+ # Refresh cmdTempl in case user just had it changed.
255
+ cmdTempl = self.controller.textEditorCmdTempl
256
+
257
+ # Attempt to create command from template and run the command
258
+ try:
259
+ command = cmdTempl.format(
260
+ exePath=exePath, modulePath=modulePath, lineNum=lineNum
261
+ )
262
+ subprocess.Popen(command)
263
+ except (ValueError, OSError):
264
+ msg = "The provided text editor command is not valid:\n {}"
265
+ msg = msg.format(cmdTempl)
266
+ print(msg)
267
+ elif workboxName is not None:
268
+ workbox = self.controller.workbox_for_name(workboxName, visible=True)
269
+ lineNum = int(lineNum)
270
+ # Make the controller visible and focus on the workbox. This is not
271
+ # using preditor.launch on the assumption that some widget has already
272
+ # initialized it to store in self.controller.
273
+ self.controller.launch(focus=False)
274
+ workbox.__goto_line__(lineNum)
275
+ workbox.setFocus()
276
+
277
+ @classmethod
278
+ def getIndentForCodeTracebackLine(cls, msg):
279
+ """Determine the indentation to recreate traceback lines
280
+
281
+ Args:
282
+ msg (str): The traceback line
283
+
284
+ Returns:
285
+ indent (str): A string of zero or more spaces used for indentation
286
+ """
287
+ indent = ""
288
+ match = re.match(r"^ *", msg)
289
+ if match:
290
+ indent = match.group() * 2
291
+ return indent
292
+
293
+ def getWorkboxLine(self, name, lineNum):
294
+ """Python 3 does not include in tracebacks the code line if it comes from
295
+ stdin, which is the case for PrEditor workboxes, so we fake it. This method
296
+ will return the line of code at lineNum, from the workbox with the provided
297
+ name.
298
+
299
+ Args:
300
+ name (str): The name of the workbox from which to get a line of code
301
+ lineNum (int): The number of the line to return
302
+
303
+ Returns:
304
+ txt (str): The line of text found
305
+ """
306
+ if not self.controller:
307
+ return None
308
+ workbox = self.controller.workbox_for_name(name)
309
+ if not workbox:
310
+ return None
311
+ if lineNum > workbox.lines():
312
+ return None
313
+ txt = workbox.text(lineNum).strip() + "\n"
314
+ return txt
315
+
316
+ def init_actions(self):
317
+ self.uiClearACT = QAction("&Clear", self)
318
+ self.uiClearACT.setIcon(QIcon(resourcePath('img/close-thick.png')))
319
+ self.uiClearACT.setToolTip(
320
+ "Clears the top section of PrEditor. This does not clear the workbox."
321
+ )
322
+ self.uiClearACT.setShortcut(QKeySequence("Ctrl+Shift+Alt+D"))
323
+ self.uiClearACT.setShortcutContext(
324
+ Qt.ShortcutContext.WidgetWithChildrenShortcut
325
+ )
326
+ self.uiClearACT.triggered.connect(self.clear)
327
+ self.addAction(self.uiClearACT)
328
+
329
+ self.uiAddBreakACT = QAction("Add Separator")
330
+ self.uiAddBreakACT.triggered.connect(self.add_separator)
331
+
332
+ def init_logging_handlers(self, attrName=None, value=None):
333
+ # Ensure the old callbacks are removed so they don't keep writing.
334
+ # The stream Manager will deal with if this widget is closed, but not
335
+ # in the case of temporarily disabling a handler.
336
+ for hi in self.logging_info.values():
337
+ hi.uninstall(self.write_log)
338
+
339
+ # Reset and add new handlers to handle log statements
340
+ self.logging_info = {}
341
+ for h in self.logging_handlers:
342
+ hi = HandlerInfo(h)
343
+ hi.install(self.write_log)
344
+ self.logging_info[hi.name] = hi
345
+
346
+ def init_excepthook(self, attrName=None, value=None):
347
+ from preditor.excepthooks import PreditorExceptHook
348
+
349
+ if value:
350
+ if self.write_error not in PreditorExceptHook.callbacks:
351
+ PreditorExceptHook.callbacks.append(self.write_error)
352
+ else:
353
+ if self.write_error in PreditorExceptHook.callbacks:
354
+ PreditorExceptHook.callbacks.remove(self.write_error)
355
+
356
+ def init_stylesheet(self, attrName=None, value=None):
357
+ if not self.controller:
358
+ return
359
+
360
+ signal = self.controller.styleSheetChanged
361
+ if self.use_console_stylesheet:
362
+ # Apply the stylesheet and ensure that future updates are respected
363
+ self._stylesheet_changed_meta = signal.connect(self.update_stylesheet)
364
+ self.update_stylesheet()
365
+
366
+ elif self._stylesheet_changed_meta:
367
+ # if disabling use_console_stylesheet, then remove the existing
368
+ # connection if it was previously connected.
369
+ signal.disconnect(self._stylesheet_changed_meta)
370
+ self._stylesheet_changed_meta = None
371
+
372
+ def maybeRepaint(self, force=False):
373
+ """Forces the console to repaint if enough time has elapsed from the
374
+ last repaint.
375
+
376
+ This method is called every time a print is written to the console. So if
377
+ more than `self.controller.repaintConsolesDelay` seconds has elapsed after
378
+ the last time maybeRepaint updated the display it will update the display
379
+ again, showing all new output.
380
+
381
+ This prefers calling `self.repaint()` so it doesn't add extra processing
382
+ of the Qt event loop. However if enabled it will call
383
+ `QApplication.processEvents` periodically. This ensures the repaint is
384
+ shown for writes if the Qt app looses focus. On windows this appears to
385
+ happen after ~5 seconds.
386
+
387
+ `self.controller.uiRepaintConsolesOnWriteCHK` can be used to disable
388
+ the entire `maybeRepaint` method. To disable only the processEvents calls
389
+ `self.controller.uiRepaintProcessEventsOccasionallyCHK` can be disabled.
390
+ The repaint delay is controlled by `self.controller.repaintConsolesDelay`.
391
+ """
392
+ if not self.controller:
393
+ return
394
+ if not self.controller.uiRepaintConsolesOnWriteCHK.isChecked():
395
+ return
396
+
397
+ if force:
398
+ if self.controller.uiRepaintProcessEventsOccasionallyCHK.isChecked():
399
+ QApplication.processEvents()
400
+ self._last_process_events_time = time.time_ns()
401
+ self._last_repaint_time = self._last_process_events_time
402
+ else:
403
+ self.repaint()
404
+ self._last_repaint_time = time.time_ns()
405
+ return
406
+
407
+ # NOTE: All numbers here should remain int values. This method is can be
408
+ # called multiple times per write/print call so it should be optimized
409
+ # as much as possible
410
+ current_time = time.time_ns()
411
+ if (
412
+ self.controller.uiRepaintProcessEventsOccasionallyCHK.isChecked()
413
+ and current_time - self._last_process_events_time > 5 * 1e9 # seconds
414
+ ):
415
+ # NOTE: On windows 10 this seems to only need called once after the
416
+ # app looses focus for the repaint to work on each call. However if
417
+ # the app regains focus while processEvents is called it will require
418
+ # another processEvents call. Calling this every X seconds also
419
+ # ensures the app doesn't stay (Not Responding for long).
420
+ QApplication.processEvents()
421
+ self._last_process_events_time = time.time_ns()
422
+ self._last_repaint_time = self._last_process_events_time
423
+ elif (
424
+ current_time - self._last_repaint_time
425
+ > self.controller.repaintConsolesDelay
426
+ ):
427
+ # Enough time has elapsed, repaint the widget and reset the delay
428
+ self.repaint()
429
+ self._last_repaint_time = time.time_ns()
430
+
431
+ def mouseMoveEvent(self, event):
432
+ """Overload of mousePressEvent to change mouse pointer to indicate it is
433
+ over a clickable error hyperlink.
434
+ """
435
+ if self.anchorAt(event.pos()):
436
+ self.viewport().setCursor(Qt.CursorShape.PointingHandCursor)
437
+ else:
438
+ self.viewport().unsetCursor()
439
+ return super().mouseMoveEvent(event)
440
+
441
+ def mousePressEvent(self, event):
442
+ """Overload of mousePressEvent to capture click position, so on release, we can
443
+ check release position. If it's the same (ie user clicked vs click-drag to
444
+ select text), we check if user clicked an error hyperlink.
445
+ """
446
+ left = event.button() == Qt.MouseButton.LeftButton
447
+ anchor = self.anchorAt(event.pos())
448
+ self.mousePressPos = event.pos()
449
+
450
+ if left and anchor:
451
+ event.ignore()
452
+ return
453
+
454
+ return super().mousePressEvent(event)
455
+
456
+ def mouseReleaseEvent(self, event):
457
+ """Overload of mouseReleaseEvent to capture if user has left clicked... Check if
458
+ click position is the same as release position, if so, call errorHyperlink.
459
+ """
460
+ samePos = event.pos() == self.mousePressPos
461
+ left = event.button() == Qt.MouseButton.LeftButton
462
+ anchor = self.anchorAt(event.pos())
463
+
464
+ if samePos and left and anchor:
465
+ self.errorHyperlink(anchor)
466
+ self.mousePressPos = None
467
+
468
+ QApplication.restoreOverrideCursor()
469
+ ret = super().mouseReleaseEvent(event)
470
+
471
+ return ret
472
+
473
+ @classmethod
474
+ def parseErrorHyperLinkInfo(cls, txt):
475
+ """Determine if txt is a File-info line from a traceback, and if so, return info
476
+ dict.
477
+ """
478
+
479
+ ret = None
480
+ if not txt.lstrip().startswith("File "):
481
+ return ret
482
+
483
+ match = cls.traceback_pattern.search(txt)
484
+ if match:
485
+ filename = match.groupdict().get('filename')
486
+ lineNum = match.groupdict().get('lineNum')
487
+ fileStart = txt.find(filename)
488
+ fileEnd = fileStart + len(filename)
489
+
490
+ ret = {
491
+ 'filename': filename,
492
+ 'fileStart': fileStart,
493
+ 'fileEnd': fileEnd,
494
+ 'lineNum': lineNum,
495
+ }
496
+ return ret
497
+
498
+ def onFirstShow(self, event) -> bool:
499
+ """Run extra code on the first showing of this widget.
500
+
501
+ Example override implementation:
502
+
503
+ class MyConsole(ConsoleBase):
504
+ def onFirstShow(self, event):
505
+ if not super().onFirstShow(event):
506
+ return False
507
+ self.doWork()
508
+ return True
509
+
510
+ Returns:
511
+ bool: Returns True only if this is the first time this widget is
512
+ shown. All overrides of this method should return the same.
513
+ """
514
+ if not self._first_show:
515
+ return False
516
+
517
+ # Configure the stream callbacks if enabled
518
+ self.update_streams()
519
+
520
+ # Redefine highlight variables now that stylesheet may have been updated
521
+ self.codeHighlighter().defineHighlightVariables()
522
+
523
+ self._first_show = False
524
+ return True
525
+
526
+ def setConsoleFont(self, font):
527
+ """Set the console's font and adjust the tabStopWidth"""
528
+
529
+ # Capture the scroll bar's current position (by percentage of max)
530
+ origPercent = None
531
+ scroll = self.verticalScrollBar()
532
+ if scroll.maximum():
533
+ origPercent = Fraction(scroll.value(), scroll.maximum())
534
+
535
+ # Set console and completer popup fonts
536
+ self.setFont(font)
537
+ self.completer().popup().setFont(font)
538
+
539
+ # Set the setTabStopWidth for the console's font
540
+ tab_width = 4
541
+ # TODO: Make tab_width a general user setting
542
+ workbox = self.controller.current_workbox()
543
+ if workbox:
544
+ tab_width = workbox.__tab_width__()
545
+ fontPixelWidth = QFontMetrics(font).horizontalAdvance(" ")
546
+ self.setTabStopDistance(fontPixelWidth * tab_width)
547
+
548
+ # Scroll to same relative position where we started
549
+ if origPercent is not None:
550
+ self.doubleSingleShotSetScrollValue(origPercent)
551
+
552
+ def showEvent(self, event):
553
+ # Ensure the onFirstShow method is run.
554
+ self.onFirstShow(event)
555
+ super().showEvent(event)
556
+
557
+ def startPrompt(self, prompt):
558
+ """create a new command prompt line with the given prompt
559
+
560
+ Args:
561
+ prompt(str): The prompt to start the line with. If this prompt
562
+ is already the only text on the last line this function does nothing.
563
+ """
564
+ self.moveCursor(QTextCursor.MoveOperation.End)
565
+
566
+ # if this is not already a new line
567
+ if self.textCursor().block().text() != prompt:
568
+ charFormat = QTextCharFormat()
569
+ self.setCurrentCharFormat(charFormat)
570
+
571
+ inputstr = prompt
572
+ if self.textCursor().block().text():
573
+ inputstr = '\n' + inputstr
574
+
575
+ self.insertPlainText(inputstr)
576
+
577
+ scroll = self.verticalScrollBar()
578
+ maximum = scroll.maximum()
579
+ if maximum is not None:
580
+ scroll.setValue(maximum)
581
+
582
+ def update_context_menu(self, menu):
583
+ """Returns the menu to use for right click context."""
584
+ # Note: this menu is built in reverse order for easy insertion
585
+ sep = menu.insertSeparator(menu.actions()[0])
586
+ menu.insertAction(sep, self.uiClearACT)
587
+ menu.insertAction(sep, self.uiAddBreakACT)
588
+ return menu
589
+
590
+ def update_streams(self, attrName=None, value=None):
591
+ # overload the sys logger and ensure the stream_manager is installed
592
+ self.stream_manager = stream.install_to_std()
593
+
594
+ needs_callback = self.stream_echo_stdout or self.stream_echo_stderr
595
+ if needs_callback:
596
+ # Redirect future writes directly to the console, add any previous
597
+ # writes to the console and possibly free up the memory consumed by
598
+ # previous writes. It's safe to call this repeatedly.
599
+ self.stream_manager.add_callback(
600
+ self.write,
601
+ replay=self.stream_replay,
602
+ disable_writes=self.stream_disable_writes,
603
+ clear=self.stream_clear,
604
+ )
605
+ else:
606
+ self.stream_manager.remove_callback(self.write)
607
+
608
+ def update_stylesheet(self):
609
+ sheet = None
610
+ if self.controller:
611
+ sheet = self.controller.styleSheet()
612
+ self.setStyleSheet(sheet)
613
+
614
+ def get_logging_info(self, name):
615
+ # Look for a specific rule to handle this logging message
616
+ parts = name.split(".")
617
+ for i in range(len(parts), 0, -1):
618
+ name = ".".join(parts[:i])
619
+ if name in self.logging_info:
620
+ return self.logging_info[name]
621
+
622
+ # If no logging handler matches the name but we are showing the root
623
+ # handler fall back to using the root handler to handle this logging call.
624
+ if "root" in self.logging_info:
625
+ return self.logging_info["root"]
626
+
627
+ # Otherwise ignore it
628
+ return None
629
+
630
+ def write_error(self, *exc_info):
631
+ text = traceback.format_exception(*exc_info)
632
+ for line in text:
633
+ self.write(line, stream_type=StreamType.CONSOLE | StreamType.STDERR)
634
+
635
+ if self._write_error_self_destruct:
636
+ # This should only be enabled by `OverrideConsoleStreams`. This callback
637
+ # was installed temporarily and once its called by sys.excepthook it
638
+ # should be removed so it doesn't get called again.
639
+ from preditor.excepthooks import PreditorExceptHook
640
+
641
+ self._write_error_self_destruct = False
642
+ PreditorExceptHook.callbacks.remove(self.write_error)
643
+
644
+ def write_log(self, log_data, stream_type=StreamType.CONSOLE):
645
+ """Write a logging message to the console depending on filters."""
646
+ handler, record = log_data
647
+ # Find the console configuration that allows processing of this record
648
+ logging_info = self.get_logging_info(record.name)
649
+ if logging_info is None:
650
+ return
651
+
652
+ # Only log the record if it matches the logging level requirements
653
+ if logging_info.level > record.levelno:
654
+ return
655
+
656
+ formatter = handler
657
+ if logging_info.formatter:
658
+ formatter = logging_info.formatter
659
+ elif self.logging_formatter:
660
+ formatter = self.logging_formatter
661
+ msg = formatter.format(record)
662
+ self.write(f'{msg}\n', stream_type=stream_type)
663
+
664
+ def write(self, msg, stream_type=StreamType.STDOUT):
665
+ """Write a message to the logger.
666
+
667
+ Args:
668
+ msg (str): The message to write.
669
+ stream_type (bool, optional): Treat this write as as stderr output.
670
+
671
+ In order to make a stack-trace provide clickable hyperlinks, it must be sent
672
+ to self._write line-by-line, like a actual exception traceback is. So, we check
673
+ if msg has the stack marker str, if so, send it line by line, otherwise, just
674
+ pass msg on to self._write.
675
+ """
676
+ stack_marker = "Stack (most recent call last)"
677
+ index = msg.find(stack_marker)
678
+ has_stack_marker = index > -1
679
+
680
+ if has_stack_marker:
681
+ lines = msg.split("\n")
682
+ for line in lines:
683
+ line = "{}\n".format(line)
684
+ self._write(line, stream_type=stream_type)
685
+ else:
686
+ self._write(msg, stream_type=stream_type)
687
+
688
+ def _write(self, msg, stream_type=StreamType.STDOUT):
689
+ """write the message to the logger"""
690
+ if not msg:
691
+ return
692
+
693
+ # Convert the stream_manager's stream to the boolean value this function expects
694
+ to_error = stream_type & StreamType.STDERR == StreamType.STDERR
695
+ to_console = stream_type & StreamType.CONSOLE == StreamType.CONSOLE
696
+ to_result = stream_type & StreamType.RESULT == StreamType.RESULT
697
+
698
+ # Check that we haven't been garbage collected before trying to write.
699
+ # This can happen while shutting down a QApplication like Nuke.
700
+ if not QtCompat.isValid(self):
701
+ return
702
+
703
+ if to_result and not self.stream_echo_result:
704
+ return
705
+
706
+ # If stream_type is Console, then always show the output
707
+ if not to_console:
708
+ # Otherwise only show the message
709
+ if to_error and not self.stream_echo_stderr:
710
+ return
711
+ if not to_error and not self.stream_echo_stdout:
712
+ return
713
+
714
+ if self.controller:
715
+ doHyperlink = self.controller.uiErrorHyperlinksCHK.isChecked()
716
+ sepPreditorTrace = self.controller.uiSeparateTracebackCHK.isChecked()
717
+ else:
718
+ doHyperlink = False
719
+ sepPreditorTrace = False
720
+ self.moveCursor(QTextCursor.MoveOperation.End)
721
+
722
+ charFormat = QTextCharFormat()
723
+ if not to_error:
724
+ charFormat.setForeground(self.stdoutColor)
725
+ else:
726
+ charFormat.setForeground(self.errorMessageColor)
727
+ self.setCurrentCharFormat(charFormat)
728
+
729
+ # If showing Error Hyperlinks... Sometimes (when a syntax error, at least),
730
+ # the last File-Info line of a traceback is issued in multiple messages
731
+ # starting with unicode paragraph separator (r"\u2029") and followed by a
732
+ # newline, so our normal string checks search won't work. Instead, we'll
733
+ # manually reconstruct the line. If msg is a newline, grab that current line
734
+ # and check it. If it matches,proceed using that line as msg
735
+ cursor = self.textCursor()
736
+ info = None
737
+
738
+ if doHyperlink and msg == '\n':
739
+ cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
740
+ line = cursor.selectedText()
741
+
742
+ # Remove possible leading unicode paragraph separator, which really
743
+ # messes up the works
744
+ if line and line[0] not in string.printable:
745
+ line = line[1:]
746
+
747
+ info = self.parseErrorHyperLinkInfo(line)
748
+ if info:
749
+ cursor.insertText("\n")
750
+ msg = "{}\n".format(line)
751
+
752
+ # If showing Error Hyperlinks, display underline output, otherwise
753
+ # display normal output. Exclude ConsolePrEdits
754
+ info = info if info else self.parseErrorHyperLinkInfo(msg)
755
+ filename = info.get("filename", "") if info else ""
756
+
757
+ # Determine if this is a workbox line of code, or code run directly
758
+ # in the console
759
+ isWorkbox = '<WorkboxSelection>' in filename or '<Workbox>' in filename
760
+ isConsolePrEdit = '<ConsolePrEdit>' in filename
761
+
762
+ # Starting in Python 3, tracebacks don't include the code executed
763
+ # for stdin, so workbox code won't appear. This attempts to include
764
+ # it. There is an exception for SyntaxErrors, which DO include the
765
+ # offending line of code, so in those cases (indicated by lack of
766
+ # inStr from the regex search) we skip faking the code line.
767
+ if isWorkbox:
768
+ match = self.workbox_pattern.search(msg)
769
+ workboxName = match.groupdict().get("workboxName")
770
+ lineNum = int(match.groupdict().get("lineNum")) - 1
771
+ inStr = match.groupdict().get("inStr", "")
772
+
773
+ workboxLine = self.getWorkboxLine(workboxName, lineNum)
774
+ if workboxLine and inStr:
775
+ indent = self.getIndentForCodeTracebackLine(msg)
776
+ msg = "{}{}{}".format(msg, indent, workboxLine)
777
+
778
+ elif isConsolePrEdit:
779
+ # Syntax error tracebacks are different than other Exception.
780
+ # They don't include ", in ..." and are issued differently than
781
+ # other Exceptions, in that they will issue the final piece of
782
+ # offending code, whereas other Exceptions do not, for some
783
+ # reason. They do not need, and shouldn't, be handled here.
784
+ match = self.console_pattern.search(msg)
785
+ inStr = match.groupdict().get("inStr", "")
786
+ if inStr:
787
+ consoleLine = self.consoleLine
788
+ indent = self.getIndentForCodeTracebackLine(msg)
789
+ msg = "{}{}{}\n".format(msg, indent, consoleLine)
790
+
791
+ # To make it easier to see relevant lines of a traceback, optionally insert
792
+ # a newline separating internal PrEditor code from the code run by user.
793
+ if self.addSepNewline:
794
+ if sepPreditorTrace:
795
+ msg = "\n" + msg
796
+ self.addSepNewline = False
797
+
798
+ preditorCalls = ("cmdresult = e", "exec(compiled,")
799
+ if msg.strip().startswith(preditorCalls):
800
+ self.addSepNewline = True
801
+
802
+ # Error tracebacks and logging.stack_info supply msg's differently,
803
+ # so modify it here, so we get consistent results.
804
+ msg = msg.replace("\n\n", "\n")
805
+
806
+ if info and doHyperlink and not isConsolePrEdit:
807
+ fileStart = info.get("fileStart")
808
+ fileEnd = info.get("fileEnd")
809
+ lineNum = info.get("lineNum")
810
+
811
+ toolTip = 'Open "{}" at line number {}'.format(filename, lineNum)
812
+ if isWorkbox:
813
+ split = filename.split(':')
814
+ workboxIdx = split[-1]
815
+ filename = ''
816
+ else:
817
+ filename = filename
818
+ workboxIdx = ''
819
+ href = '{}, {}, {}'.format(filename, workboxIdx, lineNum)
820
+
821
+ # Insert initial, non-underlined text
822
+ cursor.insertText(msg[:fileStart])
823
+
824
+ # Insert hyperlink
825
+ fmt = cursor.charFormat()
826
+ fmt.setAnchor(True)
827
+ fmt.setAnchorHref(href)
828
+ fmt.setFontUnderline(True)
829
+ fmt.setToolTip(toolTip)
830
+ cursor.insertText(msg[fileStart:fileEnd], fmt)
831
+
832
+ # Insert the rest of the msg
833
+ fmt.setAnchor(False)
834
+ fmt.setAnchorHref('')
835
+ fmt.setFontUnderline(False)
836
+ fmt.setToolTip('')
837
+ cursor.insertText(msg[fileEnd:], fmt)
838
+ else:
839
+ # Non-hyperlink output
840
+ self.insertPlainText(msg)
841
+
842
+ # Update the display of the console if enough time has passed and enabled
843
+ self.maybeRepaint()
844
+
845
+ # These Qt Properties can be customized using style sheets.
846
+ commentColor = QtPropertyInit('_commentColor', QColor(0, 206, 52))
847
+ errorMessageColor = QtPropertyInit('_errorMessageColor', QColor(Qt.GlobalColor.red))
848
+ keywordColor = QtPropertyInit('_keywordColor', QColor(17, 154, 255))
849
+ resultColor = QtPropertyInit('_resultColor', QColor(128, 128, 128))
850
+ stdoutColor = QtPropertyInit('_stdoutColor', QColor(17, 154, 255))
851
+ stringColor = QtPropertyInit('_stringColor', QColor(255, 128, 0))
852
+
853
+ @Property(str)
854
+ def logging_formatter_str(self):
855
+ """QtProperty exposing logging_formatter as a string for QtDesigner."""
856
+ try:
857
+ return self.logging_formatter._fmt
858
+ except AttributeError:
859
+ return ""
860
+
861
+ @logging_formatter_str.setter # type: ignore[no-redef]
862
+ def logging_formatter_str(self, value):
863
+ self.logging_formatter = value
864
+
865
+ logging_handlers = QtPropertyInit(
866
+ '_logging_handlers', list, callback=init_logging_handlers, typ="QStringList"
867
+ )
868
+ """Used to install LoggerWindowHandler's for this console. Each item should be a
869
+ `handler.name,level`. Level can be the int value (50) or level name (DEBUG).
870
+ """
871
+
872
+ # Configure stdout/error redirection options
873
+ stream_clear = QtPropertyInit('_stream_clear', False)
874
+ """When first shown, should this instance clear the stream manager's stored
875
+ history?"""
876
+ stream_disable_writes = QtPropertyInit('_stream_disable_writes', False)
877
+ """When first shown, should this instance disable writes on the stream?"""
878
+ stream_replay = QtPropertyInit('_stream_replay', False)
879
+ """When first shown, should this instance replay the streams stored history?"""
880
+ stream_echo_stderr = QtPropertyInit(
881
+ '_stream_echo_stderr', False, callback=update_streams
882
+ )
883
+ """Should this console print stderr writes?"""
884
+ stream_echo_stdout = QtPropertyInit(
885
+ '_stream_echo_stdout', False, callback=update_streams
886
+ )
887
+ """Should this console print stdout writes?"""
888
+ stream_echo_result = False
889
+ """Reserved for ConsolePrEdit to enable StreamType.RESULT output. There is
890
+ no reason for the baseclass to use QtPropertyInit, but this property is
891
+ checked used by write so it needs defined."""
892
+ stream_echo_tracebacks = QtPropertyInit(
893
+ "_stream_echo_tracebacks", False, callback=init_excepthook
894
+ )
895
+ """Should this console print captured exceptions? Only use this if
896
+ stream_echo_stderr is disabled or you likely will get duplicate output.
897
+ """
898
+
899
+ use_console_stylesheet = QtPropertyInit(
900
+ "_use_console_stylesheet", False, callback=init_stylesheet
901
+ )
902
+ """Set this widgets stylesheet to the PrEditor instance's style sheet.
903
+
904
+ This ensures that the style of random OutputConsoles match even when not
905
+ parented to PrEditor. Enabling this will update the widgets style sheet, but
906
+ disabling it will not update the style sheet.
907
+ """
908
+
909
+
910
+ # Build and add the class properties for regex patterns so subclasses can use them.
911
+ ConsoleBase._ConsoleBase__defineRegexPatterns()