PrEditor 1.3.0__py3-none-any.whl → 1.5.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.
Potentially problematic release.
This version of PrEditor might be problematic. Click here for more details.
- preditor/debug.py +3 -3
- preditor/gui/console.py +151 -40
- preditor/gui/drag_tab_bar.py +4 -1
- preditor/gui/group_tab_widget/__init__.py +14 -19
- preditor/gui/group_tab_widget/grouped_tab_widget.py +26 -4
- preditor/gui/group_tab_widget/one_tab_widget.py +42 -0
- preditor/gui/logger_window_plugin.py +3 -0
- preditor/gui/loggerwindow.py +28 -41
- preditor/gui/workbox_mixin.py +101 -3
- preditor/gui/workboxwidget.py +4 -1
- preditor/scintilla/documenteditor.py +1 -9
- preditor/stream/director.py +87 -16
- preditor/version.py +3 -3
- {preditor-1.3.0.dist-info → preditor-1.5.0.dist-info}/METADATA +1 -1
- {preditor-1.3.0.dist-info → preditor-1.5.0.dist-info}/RECORD +19 -19
- {preditor-1.3.0.dist-info → preditor-1.5.0.dist-info}/WHEEL +0 -0
- {preditor-1.3.0.dist-info → preditor-1.5.0.dist-info}/entry_points.txt +0 -0
- {preditor-1.3.0.dist-info → preditor-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {preditor-1.3.0.dist-info → preditor-1.5.0.dist-info}/top_level.txt +0 -0
preditor/debug.py
CHANGED
|
@@ -32,9 +32,9 @@ class FileLogger:
|
|
|
32
32
|
return msg.format(today=datetime.datetime.today(), version=sys.version)
|
|
33
33
|
|
|
34
34
|
def write(self, msg):
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
with open(self._logfile, 'a', encoding="utf-8") as f:
|
|
36
|
+
f.write(msg)
|
|
37
|
+
|
|
38
38
|
if self._print:
|
|
39
39
|
self._stdhandle.write(msg)
|
|
40
40
|
|
preditor/gui/console.py
CHANGED
|
@@ -53,6 +53,16 @@ class ConsolePrEdit(QTextEdit):
|
|
|
53
53
|
# If populated, also write to this interface
|
|
54
54
|
self.outputPipe = None
|
|
55
55
|
|
|
56
|
+
# For workboxes, use this regex pattern, so we can extract workboxName
|
|
57
|
+
# and lineNum
|
|
58
|
+
pattern = r'File "<Workbox(?:Selection)?>:(?P<workboxName>.*)", '
|
|
59
|
+
pattern += r'line (?P<lineNum>\d{1,6}), in'
|
|
60
|
+
self.workbox_pattern = re.compile(pattern)
|
|
61
|
+
|
|
62
|
+
# Define a pattern to capture info from tracebacks
|
|
63
|
+
pattern = r'File "(?P<filename>.*)", line (?P<lineNum>\d{1,10}), in'
|
|
64
|
+
self.traceback_pattern = re.compile(pattern)
|
|
65
|
+
|
|
56
66
|
self._consolePrompt = '>>> '
|
|
57
67
|
# Note: Changing _outputPrompt may require updating resource\lang\python.xml
|
|
58
68
|
# If still using a #
|
|
@@ -90,7 +100,7 @@ class ConsolePrEdit(QTextEdit):
|
|
|
90
100
|
# to the console and free up the memory consumed by previous writes as we
|
|
91
101
|
# assume this is likely to be the only callback added to the manager.
|
|
92
102
|
self.stream_manager.add_callback(
|
|
93
|
-
self.
|
|
103
|
+
self.pre_write, replay=True, disable_writes=True, clear=True
|
|
94
104
|
)
|
|
95
105
|
# Store the current outputs
|
|
96
106
|
self.stdout = sys.stdout
|
|
@@ -122,6 +132,11 @@ class ConsolePrEdit(QTextEdit):
|
|
|
122
132
|
if not self.cursorWidth():
|
|
123
133
|
self.setCursorWidth(1)
|
|
124
134
|
|
|
135
|
+
# The act of changing from no scroll bar to a scroll bar can add up to 1
|
|
136
|
+
# second of time to the process of outputting text, so, just always have
|
|
137
|
+
# it on.
|
|
138
|
+
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
|
139
|
+
|
|
125
140
|
def doubleSingleShotSetScrollValue(self, origPercent):
|
|
126
141
|
"""This double QTimer.singleShot monkey business seems to be the only way
|
|
127
142
|
to get scroll.maximum() to update properly so that we calc newValue
|
|
@@ -239,7 +254,7 @@ class ConsolePrEdit(QTextEdit):
|
|
|
239
254
|
# info is a comma separated string, in the form: "filename, workboxIdx, lineNum"
|
|
240
255
|
info = self.anchor.split(', ')
|
|
241
256
|
modulePath = info[0]
|
|
242
|
-
|
|
257
|
+
workboxName = info[1]
|
|
243
258
|
lineNum = info[2]
|
|
244
259
|
|
|
245
260
|
# fetch info from LoggerWindow
|
|
@@ -250,23 +265,27 @@ class ConsolePrEdit(QTextEdit):
|
|
|
250
265
|
cmdTempl = window.textEditorCmdTempl
|
|
251
266
|
|
|
252
267
|
# Bail if not setup properly
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
if workboxName is None:
|
|
269
|
+
msg = (
|
|
270
|
+
"Cannot use traceback hyperlink (Correct the path with Options "
|
|
271
|
+
"> Set Preferred Text Editor Path).\n"
|
|
272
|
+
)
|
|
273
|
+
if not exePath:
|
|
274
|
+
msg += "No text editor path defined."
|
|
275
|
+
print(msg)
|
|
276
|
+
return
|
|
277
|
+
if not os.path.exists(exePath):
|
|
278
|
+
msg += "Text editor executable does not exist: {}".format(exePath)
|
|
279
|
+
print(msg)
|
|
280
|
+
return
|
|
281
|
+
if not cmdTempl:
|
|
282
|
+
msg += "No text editor Command Prompt command template defined."
|
|
283
|
+
print(msg)
|
|
284
|
+
return
|
|
285
|
+
if modulePath and not os.path.exists(modulePath):
|
|
286
|
+
msg += "Specified module path does not exist: {}".format(modulePath)
|
|
287
|
+
print(msg)
|
|
288
|
+
return
|
|
270
289
|
|
|
271
290
|
if modulePath:
|
|
272
291
|
# Check if cmdTempl filepaths aren't wrapped in double=quotes to handle
|
|
@@ -296,12 +315,9 @@ class ConsolePrEdit(QTextEdit):
|
|
|
296
315
|
msg = "The provided text editor command template is not valid:\n {}"
|
|
297
316
|
msg = msg.format(cmdTempl)
|
|
298
317
|
print(msg)
|
|
299
|
-
elif
|
|
300
|
-
|
|
318
|
+
elif workboxName is not None:
|
|
319
|
+
workbox = window.workbox_for_name(workboxName, visible=True)
|
|
301
320
|
lineNum = int(lineNum)
|
|
302
|
-
workbox = window.uiWorkboxTAB.set_current_groups_from_index(
|
|
303
|
-
int(group), int(editor)
|
|
304
|
-
)
|
|
305
321
|
workbox.__goto_line__(lineNum)
|
|
306
322
|
workbox.setFocus()
|
|
307
323
|
|
|
@@ -373,7 +389,34 @@ class ConsolePrEdit(QTextEdit):
|
|
|
373
389
|
"""returns the completer instance that is associated with this editor"""
|
|
374
390
|
return self._completer
|
|
375
391
|
|
|
376
|
-
def
|
|
392
|
+
def getWorkboxLine(self, name, lineNum):
|
|
393
|
+
"""Python 3 does not include in tracebacks the code line if it comes from
|
|
394
|
+
stdin, which is the case for PrEditor workboxes, so we fake it. This method
|
|
395
|
+
will return the line of code at lineNum, from the workbox with the provided
|
|
396
|
+
name.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
name (str): The name of the workbox from which to get a line of code
|
|
400
|
+
lineNum (int): The number of the line to return
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
txt (str): The line of text found
|
|
404
|
+
"""
|
|
405
|
+
workbox = self.window().workbox_for_name(name)
|
|
406
|
+
if not workbox:
|
|
407
|
+
return None
|
|
408
|
+
if lineNum > workbox.lines():
|
|
409
|
+
return None
|
|
410
|
+
txt = workbox.text(lineNum).strip() + "\n"
|
|
411
|
+
return txt
|
|
412
|
+
|
|
413
|
+
def executeString(
|
|
414
|
+
self, commandText, consoleLine=None, filename='<ConsolePrEdit>', extraPrint=True
|
|
415
|
+
):
|
|
416
|
+
# These vars helps with faking code lines in tracebacks for stdin input, which
|
|
417
|
+
# workboxes are, and py3 doesn't include in the traceback
|
|
418
|
+
self.consoleLine = consoleLine or ""
|
|
419
|
+
|
|
377
420
|
if self.clearExecutionTime is not None:
|
|
378
421
|
self.clearExecutionTime()
|
|
379
422
|
cursor = self.textCursor()
|
|
@@ -428,6 +471,10 @@ class ConsolePrEdit(QTextEdit):
|
|
|
428
471
|
|
|
429
472
|
def executeCommand(self):
|
|
430
473
|
"""executes the current line of code"""
|
|
474
|
+
|
|
475
|
+
# Not using workbox, so clear this
|
|
476
|
+
self.consoleLine = ""
|
|
477
|
+
|
|
431
478
|
# grab the command from the line
|
|
432
479
|
block = self.textCursor().block().text()
|
|
433
480
|
p = '{prompt}(.*)'.format(prompt=re.escape(self.prompt()))
|
|
@@ -450,7 +497,9 @@ class ConsolePrEdit(QTextEdit):
|
|
|
450
497
|
self._prevCommands = self._prevCommands[-1 * self._prevCommandsMax :]
|
|
451
498
|
|
|
452
499
|
# evaluate the command
|
|
453
|
-
cmdresult, wasEval = self.executeString(
|
|
500
|
+
cmdresult, wasEval = self.executeString(
|
|
501
|
+
commandText, consoleLine=commandText
|
|
502
|
+
)
|
|
454
503
|
|
|
455
504
|
# print the resulting commands
|
|
456
505
|
if cmdresult is not None:
|
|
@@ -787,28 +836,65 @@ class ConsolePrEdit(QTextEdit):
|
|
|
787
836
|
"""Determine if txt is a File-info line from a traceback, and if so, return info
|
|
788
837
|
dict.
|
|
789
838
|
"""
|
|
790
|
-
|
|
839
|
+
|
|
791
840
|
ret = None
|
|
841
|
+
if not txt.lstrip().startswith("File "):
|
|
842
|
+
return ret
|
|
843
|
+
|
|
844
|
+
match = self.traceback_pattern.search(txt)
|
|
845
|
+
if match:
|
|
846
|
+
filename = match.groupdict().get('filename')
|
|
847
|
+
lineNum = match.groupdict().get('lineNum')
|
|
848
|
+
fileStart = txt.find(filename)
|
|
849
|
+
fileEnd = fileStart + len(filename)
|
|
792
850
|
|
|
793
|
-
filenameEnd = txt.find(lineMarker)
|
|
794
|
-
if txt[:8] == ' File "' and filenameEnd >= 0:
|
|
795
|
-
filename = txt[8:filenameEnd]
|
|
796
|
-
lineNumStart = filenameEnd + len(lineMarker)
|
|
797
|
-
lineNumEnd = txt.find(',', lineNumStart)
|
|
798
|
-
if lineNumEnd == -1:
|
|
799
|
-
lineNumEnd = len(txt)
|
|
800
|
-
lineNum = txt[lineNumStart:lineNumEnd]
|
|
801
851
|
ret = {
|
|
802
852
|
'filename': filename,
|
|
803
|
-
'fileStart':
|
|
804
|
-
'fileEnd':
|
|
853
|
+
'fileStart': fileStart,
|
|
854
|
+
'fileEnd': fileEnd,
|
|
805
855
|
'lineNum': lineNum,
|
|
806
856
|
}
|
|
807
|
-
|
|
808
857
|
return ret
|
|
809
858
|
|
|
859
|
+
@staticmethod
|
|
860
|
+
def getIndentForCodeTracebackLine(msg):
|
|
861
|
+
"""Determine the indentation to recreate traceback lines
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
msg (str): The traceback line
|
|
865
|
+
|
|
866
|
+
Returns:
|
|
867
|
+
indent (str): A string of zero or more spaces used for indentation
|
|
868
|
+
"""
|
|
869
|
+
indent = ""
|
|
870
|
+
match = re.match(r"^ *", msg)
|
|
871
|
+
if match:
|
|
872
|
+
indent = match.group() * 2
|
|
873
|
+
return indent
|
|
874
|
+
|
|
875
|
+
def pre_write(self, msg, error=False):
|
|
876
|
+
"""In order to make a stack-trace provide clickable hyperlinks, it must be sent
|
|
877
|
+
to self.write line-by-line, like a actual exception traceback is. So, we check
|
|
878
|
+
if msg has the stack marker str, if so, send it line by line, otherwise, just
|
|
879
|
+
pass msg on to self.write.
|
|
880
|
+
"""
|
|
881
|
+
stack_marker = "Stack (most recent call last)"
|
|
882
|
+
index = msg.find(stack_marker)
|
|
883
|
+
has_stack_marker = index > -1
|
|
884
|
+
|
|
885
|
+
if has_stack_marker:
|
|
886
|
+
lines = msg.split("\n")
|
|
887
|
+
for line in lines:
|
|
888
|
+
line = "{}\n".format(line)
|
|
889
|
+
self.write(line, error=error)
|
|
890
|
+
else:
|
|
891
|
+
self.write(msg, error=error)
|
|
892
|
+
|
|
810
893
|
def write(self, msg, error=False):
|
|
811
894
|
"""write the message to the logger"""
|
|
895
|
+
if not msg:
|
|
896
|
+
return
|
|
897
|
+
|
|
812
898
|
# Convert the stream_manager's stream to the boolean value this function expects
|
|
813
899
|
error = error == stream.STDERR
|
|
814
900
|
# Check that we haven't been garbage collected before trying to write.
|
|
@@ -859,25 +945,50 @@ class ConsolePrEdit(QTextEdit):
|
|
|
859
945
|
# display normal output. Exclude ConsolePrEdits
|
|
860
946
|
info = info if info else self.parseErrorHyperLinkInfo(msg)
|
|
861
947
|
filename = info.get("filename", "") if info else ""
|
|
948
|
+
|
|
949
|
+
# Determine if this is a workbox line of code, or code run directly
|
|
950
|
+
# in the console
|
|
951
|
+
isWorkbox = '<WorkboxSelection>' in filename or '<Workbox>' in filename
|
|
862
952
|
isConsolePrEdit = '<ConsolePrEdit>' in filename
|
|
863
953
|
|
|
954
|
+
# Starting in Python 3, tracebacks don't include the code executed
|
|
955
|
+
# for stdin, so workbox code won't appear. This attempts to include
|
|
956
|
+
# it.
|
|
957
|
+
if isWorkbox:
|
|
958
|
+
match = self.workbox_pattern.search(msg)
|
|
959
|
+
workboxName = match.groupdict().get("workboxName")
|
|
960
|
+
lineNum = int(match.groupdict().get("lineNum")) - 1
|
|
961
|
+
|
|
962
|
+
workboxLine = self.getWorkboxLine(workboxName, lineNum)
|
|
963
|
+
if workboxLine:
|
|
964
|
+
indent = self.getIndentForCodeTracebackLine(msg)
|
|
965
|
+
msg = "{}{}{}".format(msg, indent, workboxLine)
|
|
966
|
+
|
|
967
|
+
elif isConsolePrEdit:
|
|
968
|
+
consoleLine = self.consoleLine
|
|
969
|
+
indent = self.getIndentForCodeTracebackLine(msg)
|
|
970
|
+
msg = "{}{}{}\n".format(msg, indent, consoleLine)
|
|
971
|
+
|
|
864
972
|
# To make it easier to see relevant lines of a traceback, optionally insert
|
|
865
973
|
# a newline separating internal PrEditor code from the code run by user.
|
|
866
974
|
if self.addSepNewline:
|
|
867
975
|
if sepPreditorTrace:
|
|
868
|
-
msg
|
|
976
|
+
msg = "\n" + msg
|
|
869
977
|
self.addSepNewline = False
|
|
870
978
|
|
|
871
979
|
preditorCalls = ("cmdresult = e", "exec(compiled,")
|
|
872
980
|
if msg.strip().startswith(preditorCalls):
|
|
873
981
|
self.addSepNewline = True
|
|
874
982
|
|
|
983
|
+
# Error tracebacks and logging.stack_info supply msg's differently,
|
|
984
|
+
# so modify it here, so we get consistent results.
|
|
985
|
+
msg = msg.replace("\n\n", "\n")
|
|
986
|
+
|
|
875
987
|
if info and doHyperlink and not isConsolePrEdit:
|
|
876
988
|
fileStart = info.get("fileStart")
|
|
877
989
|
fileEnd = info.get("fileEnd")
|
|
878
990
|
lineNum = info.get("lineNum")
|
|
879
991
|
|
|
880
|
-
isWorkbox = '<WorkboxSelection>' in filename or '<Workbox>' in filename
|
|
881
992
|
if isWorkbox:
|
|
882
993
|
split = filename.split(':')
|
|
883
994
|
workboxIdx = split[-1]
|
preditor/gui/drag_tab_bar.py
CHANGED
|
@@ -139,8 +139,11 @@ class DragTabBar(QTabBar):
|
|
|
139
139
|
"""Used by the tab_menu to rename the tab at index `_context_menu_tab`."""
|
|
140
140
|
if self._context_menu_tab != -1:
|
|
141
141
|
current = self.tabText(self._context_menu_tab)
|
|
142
|
-
msg = 'Rename the {} tab to:'.format(current)
|
|
142
|
+
msg = 'Rename the {} tab to (new name must be unique):'.format(current)
|
|
143
|
+
|
|
143
144
|
name, success = QInputDialog.getText(self, 'Rename Tab', msg, text=current)
|
|
145
|
+
name = self.parent().get_next_available_tab_name(name)
|
|
146
|
+
|
|
144
147
|
if success:
|
|
145
148
|
self.setTabText(self._context_menu_tab, name)
|
|
146
149
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import absolute_import
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import re
|
|
5
4
|
|
|
6
5
|
from Qt.QtCore import Qt
|
|
7
6
|
from Qt.QtGui import QIcon
|
|
@@ -46,6 +45,9 @@ class GroupTabWidget(OneTabWidget):
|
|
|
46
45
|
self.editor_cls = WorkboxTextEdit
|
|
47
46
|
self.core_name = core_name
|
|
48
47
|
self.setStyleSheet(DEFAULT_STYLE_SHEET)
|
|
48
|
+
|
|
49
|
+
self.default_title = 'Group01'
|
|
50
|
+
|
|
49
51
|
corner = QWidget(self)
|
|
50
52
|
lyt = QHBoxLayout(corner)
|
|
51
53
|
lyt.setSpacing(0)
|
|
@@ -69,7 +71,7 @@ class GroupTabWidget(OneTabWidget):
|
|
|
69
71
|
self.uiCornerBTN = corner
|
|
70
72
|
self.setCornerWidget(self.uiCornerBTN, Qt.Corner.TopRightCorner)
|
|
71
73
|
|
|
72
|
-
def add_new_tab(self, group, title=
|
|
74
|
+
def add_new_tab(self, group, title=None, prefs=None):
|
|
73
75
|
"""Adds a new tab to the requested group, creating the group if the group
|
|
74
76
|
doesn't exist.
|
|
75
77
|
|
|
@@ -80,9 +82,6 @@ class GroupTabWidget(OneTabWidget):
|
|
|
80
82
|
If True is passed, then the current group tab is used.
|
|
81
83
|
title (str, optional): The name to give the newly created tab inside
|
|
82
84
|
the group.
|
|
83
|
-
group_fmt(str, optional): If None is passed to group, this string is
|
|
84
|
-
used to search for existing tabs to calculate the last number
|
|
85
|
-
and generate the new group tab name.
|
|
86
85
|
|
|
87
86
|
Returns:
|
|
88
87
|
GroupedTabWidget: The tab group for this group.
|
|
@@ -90,21 +89,14 @@ class GroupTabWidget(OneTabWidget):
|
|
|
90
89
|
"""
|
|
91
90
|
parent = None
|
|
92
91
|
if not group:
|
|
93
|
-
|
|
94
|
-
group_fmt = r'Group {}'
|
|
95
|
-
last = 0
|
|
96
|
-
for i in range(self.count()):
|
|
97
|
-
match = re.match(group_fmt.format(r'(\d+)'), self.tabText(i))
|
|
98
|
-
if match:
|
|
99
|
-
last = max(last, int(match.group(1)))
|
|
100
|
-
group = group_fmt.format(last + 1)
|
|
92
|
+
group = self.get_next_available_tab_name(self.default_title)
|
|
101
93
|
elif group is True:
|
|
102
94
|
group = self.currentIndex()
|
|
103
95
|
if isinstance(group, int):
|
|
104
96
|
group_title = self.tabText(group)
|
|
105
97
|
parent = self.widget(group)
|
|
106
98
|
elif isinstance(group, str):
|
|
107
|
-
group_title = group
|
|
99
|
+
group_title = group.replace(" ", "_")
|
|
108
100
|
index = self.index_for_text(group)
|
|
109
101
|
if index != -1:
|
|
110
102
|
parent = self.widget(index)
|
|
@@ -146,7 +138,7 @@ class GroupTabWidget(OneTabWidget):
|
|
|
146
138
|
self,
|
|
147
139
|
'Close all editors under this tab?',
|
|
148
140
|
'Are you sure you want to close all tabs under the "{}" tab?'.format(
|
|
149
|
-
self.tabText(
|
|
141
|
+
self.tabText(index)
|
|
150
142
|
),
|
|
151
143
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
|
152
144
|
)
|
|
@@ -154,11 +146,11 @@ class GroupTabWidget(OneTabWidget):
|
|
|
154
146
|
# Clean up all temp files created by this group's editors if they
|
|
155
147
|
# are not using actual saved files.
|
|
156
148
|
tab_widget = self.widget(self.currentIndex())
|
|
157
|
-
for
|
|
158
|
-
editor = tab_widget.widget(
|
|
149
|
+
for editor_index in range(tab_widget.count()):
|
|
150
|
+
editor = tab_widget.widget(editor_index)
|
|
159
151
|
editor.__remove_tempfile__()
|
|
160
152
|
|
|
161
|
-
super(GroupTabWidget, self).close_tab(
|
|
153
|
+
super(GroupTabWidget, self).close_tab(index)
|
|
162
154
|
|
|
163
155
|
def current_groups_widget(self):
|
|
164
156
|
"""Returns the current widget of the currently selected group or None."""
|
|
@@ -166,7 +158,8 @@ class GroupTabWidget(OneTabWidget):
|
|
|
166
158
|
if editor_tab:
|
|
167
159
|
return editor_tab.currentWidget()
|
|
168
160
|
|
|
169
|
-
def default_tab(self, title=
|
|
161
|
+
def default_tab(self, title=None, prefs=None):
|
|
162
|
+
title = title or self.default_title
|
|
170
163
|
widget = GroupedTabWidget(
|
|
171
164
|
parent=self,
|
|
172
165
|
editor_kwargs=self.editor_kwargs,
|
|
@@ -223,6 +216,8 @@ class GroupTabWidget(OneTabWidget):
|
|
|
223
216
|
group_name = group['name']
|
|
224
217
|
tab_widget = None
|
|
225
218
|
|
|
219
|
+
group_name = self.get_next_available_tab_name(group_name)
|
|
220
|
+
|
|
226
221
|
for tab in group.get('tabs', []):
|
|
227
222
|
# Only add this tab if, there is data on disk to load. The user can
|
|
228
223
|
# open multiple instances of PrEditor using the same prefs. The
|
|
@@ -27,7 +27,12 @@ class GroupedTabWidget(OneTabWidget):
|
|
|
27
27
|
self.uiCornerBTN.released.connect(lambda: self.add_new_editor())
|
|
28
28
|
self.setCornerWidget(self.uiCornerBTN, Qt.Corner.TopRightCorner)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
self.default_title = "Workbox01"
|
|
31
|
+
|
|
32
|
+
def add_new_editor(self, title=None, prefs=None):
|
|
33
|
+
title = title or self.default_title
|
|
34
|
+
|
|
35
|
+
title = self.get_next_available_tab_name(title)
|
|
31
36
|
editor, title = self.default_tab(title)
|
|
32
37
|
index = self.addTab(editor, title)
|
|
33
38
|
self.setCurrentIndex(index)
|
|
@@ -45,11 +50,18 @@ class GroupedTabWidget(OneTabWidget):
|
|
|
45
50
|
self, 'Tab can not be closed.', msg, QMessageBox.StandardButton.Ok
|
|
46
51
|
)
|
|
47
52
|
return
|
|
53
|
+
|
|
54
|
+
workbox = self.widget(index)
|
|
55
|
+
name = workbox.__workbox_name__()
|
|
56
|
+
msg = (
|
|
57
|
+
f"Would you like to donate the contents of tab\n{name}\nto the "
|
|
58
|
+
"/dev/null fund for wayward code?"
|
|
59
|
+
)
|
|
60
|
+
|
|
48
61
|
ret = QMessageBox.question(
|
|
49
62
|
self,
|
|
50
63
|
'Donate to the cause?',
|
|
51
|
-
|
|
52
|
-
"for wayward code?",
|
|
64
|
+
msg,
|
|
53
65
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
|
54
66
|
)
|
|
55
67
|
if ret == QMessageBox.StandardButton.Yes:
|
|
@@ -59,7 +71,8 @@ class GroupedTabWidget(OneTabWidget):
|
|
|
59
71
|
|
|
60
72
|
super(GroupedTabWidget, self).close_tab(index)
|
|
61
73
|
|
|
62
|
-
def default_tab(self, title=
|
|
74
|
+
def default_tab(self, title=None, prefs=None):
|
|
75
|
+
title = title or self.default_title
|
|
63
76
|
kwargs = self.editor_kwargs if self.editor_kwargs else {}
|
|
64
77
|
editor = self.editor_cls(parent=self, core_name=self.core_name, **kwargs)
|
|
65
78
|
return editor, title
|
|
@@ -76,5 +89,14 @@ class GroupedTabWidget(OneTabWidget):
|
|
|
76
89
|
if hasattr(self.window(), "setWorkboxFontBasedOnConsole"):
|
|
77
90
|
self.window().setWorkboxFontBasedOnConsole()
|
|
78
91
|
|
|
92
|
+
def tab_widget(self):
|
|
93
|
+
"""Return the tab widget which contains this group tab
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
self._tab_widget (GroupTabWidget): The tab widget which contains
|
|
97
|
+
this workbox
|
|
98
|
+
"""
|
|
99
|
+
return self.parent().parent()
|
|
100
|
+
|
|
79
101
|
def update_closable_tabs(self):
|
|
80
102
|
self.setTabsClosable(self.count() != 1)
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
from __future__ import absolute_import
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
|
|
3
5
|
from Qt.QtWidgets import QTabWidget
|
|
4
6
|
|
|
7
|
+
TAB_ITERATION_PATTERN = re.compile(r"(\d+)(?!.*\d)")
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
class OneTabWidget(QTabWidget):
|
|
7
11
|
"""A QTabWidget that shows the close button only if there is more than one
|
|
@@ -17,6 +21,44 @@ class OneTabWidget(QTabWidget):
|
|
|
17
21
|
super(OneTabWidget, self).__init__(*args, **kwargs)
|
|
18
22
|
self.tabCloseRequested.connect(self.close_tab)
|
|
19
23
|
|
|
24
|
+
def get_next_available_tab_name(self, name):
|
|
25
|
+
"""Get the next available tab name, incrementing an iteration if needed.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name (str): The desired name
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
name (str): The name, or updated name if needed
|
|
32
|
+
"""
|
|
33
|
+
name = name.replace(" ", "_")
|
|
34
|
+
|
|
35
|
+
existing_names = [self.tabText(i) for i in range(self.count())]
|
|
36
|
+
|
|
37
|
+
# Use regex to find the last set of digits. If found, the base name is
|
|
38
|
+
# a slice of name minus the digits string. Otherwise, the base name is
|
|
39
|
+
# the full name and iteration is zero.
|
|
40
|
+
match = TAB_ITERATION_PATTERN.search(name)
|
|
41
|
+
if match:
|
|
42
|
+
# We found trailing digits, so slice to get base name, and convert
|
|
43
|
+
# iteration to int
|
|
44
|
+
iter_str = match.group()
|
|
45
|
+
base = name[: -len(iter_str)]
|
|
46
|
+
iteration = int(iter_str)
|
|
47
|
+
else:
|
|
48
|
+
# No trailing digits found, so base name is full name and iteration
|
|
49
|
+
# is zero.
|
|
50
|
+
base = name
|
|
51
|
+
iteration = 0
|
|
52
|
+
|
|
53
|
+
if name in existing_names:
|
|
54
|
+
for _ in range(99):
|
|
55
|
+
iteration += 1
|
|
56
|
+
new_iter_str = str(iteration).zfill(2)
|
|
57
|
+
name = base + new_iter_str
|
|
58
|
+
if name not in existing_names:
|
|
59
|
+
break
|
|
60
|
+
return name
|
|
61
|
+
|
|
20
62
|
def addTab(self, *args, **kwargs): # noqa: N802
|
|
21
63
|
ret = super(OneTabWidget, self).addTab(*args, **kwargs)
|
|
22
64
|
self.update_closable_tabs()
|
preditor/gui/loggerwindow.py
CHANGED
|
@@ -48,6 +48,7 @@ from .completer import CompleterMode
|
|
|
48
48
|
from .level_buttons import LoggingLevelButton
|
|
49
49
|
from .set_text_editor_path_dialog import SetTextEditorPathDialog
|
|
50
50
|
from .status_label import StatusLabel
|
|
51
|
+
from .workbox_mixin import WorkboxName
|
|
51
52
|
|
|
52
53
|
logger = logging.getLogger(__name__)
|
|
53
54
|
|
|
@@ -59,31 +60,6 @@ class WorkboxPages:
|
|
|
59
60
|
Workboxes = 1
|
|
60
61
|
|
|
61
62
|
|
|
62
|
-
class WorkboxName(str):
|
|
63
|
-
"""The joined name of a workbox `group/workbox` with access to its parts.
|
|
64
|
-
|
|
65
|
-
This subclass provides properties for the group and workbox values separately.
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
def __new__(cls, group, workbox):
|
|
69
|
-
txt = "/".join((group, workbox))
|
|
70
|
-
ret = super().__new__(cls, txt)
|
|
71
|
-
# Preserve the imitable nature of str's by using properties without setters.
|
|
72
|
-
ret._group = group
|
|
73
|
-
ret._workbox = workbox
|
|
74
|
-
return ret
|
|
75
|
-
|
|
76
|
-
@property
|
|
77
|
-
def group(self):
|
|
78
|
-
"""The tab name of the group tab that contains the workbox."""
|
|
79
|
-
return self._group
|
|
80
|
-
|
|
81
|
-
@property
|
|
82
|
-
def workbox(self):
|
|
83
|
-
"""The workbox of the tab for this workbox inside of the group."""
|
|
84
|
-
return self._workbox
|
|
85
|
-
|
|
86
|
-
|
|
87
63
|
class LoggerWindow(Window):
|
|
88
64
|
_instance = None
|
|
89
65
|
styleSheetChanged = Signal(str)
|
|
@@ -290,10 +266,9 @@ class LoggerWindow(Window):
|
|
|
290
266
|
|
|
291
267
|
self.dont_ask_again = []
|
|
292
268
|
|
|
293
|
-
# Load any plugins
|
|
294
|
-
self.
|
|
295
|
-
|
|
296
|
-
self.plugins[name] = plugin(self)
|
|
269
|
+
# Load any plugins, and set window title
|
|
270
|
+
self.loadPlugins()
|
|
271
|
+
self.setWindowTitle(self.defineWindowTitle())
|
|
297
272
|
|
|
298
273
|
self.restorePrefs()
|
|
299
274
|
|
|
@@ -306,17 +281,6 @@ class LoggerWindow(Window):
|
|
|
306
281
|
action.triggered.connect(partial(self.setStyleSheet, style_name))
|
|
307
282
|
|
|
308
283
|
self.uiConsoleTOOLBAR.show()
|
|
309
|
-
loggerName = QApplication.instance().translate(
|
|
310
|
-
'PrEditorWindow', DEFAULT_CORE_NAME
|
|
311
|
-
)
|
|
312
|
-
self.setWindowTitle(
|
|
313
|
-
'{} - {} - {} {}-bit'.format(
|
|
314
|
-
loggerName,
|
|
315
|
-
self.name,
|
|
316
|
-
'{}.{}.{}'.format(*sys.version_info[:3]),
|
|
317
|
-
osystem.getPointerSize(),
|
|
318
|
-
)
|
|
319
|
-
)
|
|
320
284
|
|
|
321
285
|
self.setWorkboxFontBasedOnConsole()
|
|
322
286
|
self.setEditorChooserFontBasedOnConsole()
|
|
@@ -357,6 +321,29 @@ class LoggerWindow(Window):
|
|
|
357
321
|
|
|
358
322
|
self.update_workbox_stack()
|
|
359
323
|
|
|
324
|
+
def loadPlugins(self):
|
|
325
|
+
"""Load any plugins that modify the LoggerWindow."""
|
|
326
|
+
self.plugins = {}
|
|
327
|
+
for name, plugin in plugins.loggerwindow():
|
|
328
|
+
if name not in self.plugins:
|
|
329
|
+
self.plugins[name] = plugin(self)
|
|
330
|
+
|
|
331
|
+
def defineWindowTitle(self):
|
|
332
|
+
"""Define the window title, including and info plugins may add."""
|
|
333
|
+
|
|
334
|
+
# Define the title
|
|
335
|
+
loggerName = QApplication.instance().translate(
|
|
336
|
+
'PrEditorWindow', DEFAULT_CORE_NAME
|
|
337
|
+
)
|
|
338
|
+
pyVersion = '{}.{}.{}'.format(*sys.version_info[:3])
|
|
339
|
+
size = osystem.getPointerSize()
|
|
340
|
+
title = f"{loggerName} - {self.name} - {pyVersion} {size}-bit"
|
|
341
|
+
|
|
342
|
+
# Add any info plugins may add to title
|
|
343
|
+
for _name, plugin in self.plugins.items():
|
|
344
|
+
title = plugin.updateWindowTitle(title)
|
|
345
|
+
return title
|
|
346
|
+
|
|
360
347
|
def comment_toggle(self):
|
|
361
348
|
self.current_workbox().__comment_toggle__()
|
|
362
349
|
|
|
@@ -1046,7 +1033,7 @@ class LoggerWindow(Window):
|
|
|
1046
1033
|
# Create timer to autohide status messages
|
|
1047
1034
|
self.statusTimer = QTimer()
|
|
1048
1035
|
self.statusTimer.setSingleShot(True)
|
|
1049
|
-
self.statusTimer.setInterval(
|
|
1036
|
+
self.statusTimer.setInterval(5000)
|
|
1050
1037
|
self.statusTimer.timeout.connect(self.clearStatusText)
|
|
1051
1038
|
|
|
1052
1039
|
def clearStatusText(self):
|
preditor/gui/workbox_mixin.py
CHANGED
|
@@ -12,6 +12,44 @@ from Qt.QtWidgets import QStackedWidget
|
|
|
12
12
|
from ..prefs import prefs_path
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class WorkboxName(str):
|
|
16
|
+
"""The joined name of a workbox `group/workbox` with access to its parts.
|
|
17
|
+
|
|
18
|
+
You may pass the group, workbox, or the fully formed workbox name:
|
|
19
|
+
examples:
|
|
20
|
+
workboxName = WorkboxName("Group01", "Workbox05")
|
|
21
|
+
workboxName = WorkboxName("Group01/Workbox05")
|
|
22
|
+
This subclass provides properties for the group and workbox values separately.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __new__(cls, name, sub_name=None):
|
|
26
|
+
if sub_name is not None:
|
|
27
|
+
txt = "/".join((name, sub_name))
|
|
28
|
+
else:
|
|
29
|
+
txt = name
|
|
30
|
+
try:
|
|
31
|
+
name, sub_name = txt.split("/")
|
|
32
|
+
except ValueError:
|
|
33
|
+
msg = "A fully formed name, or a group and name, must be passed in."
|
|
34
|
+
raise ValueError(msg) from None
|
|
35
|
+
|
|
36
|
+
ret = super().__new__(cls, txt)
|
|
37
|
+
# Preserve the imitable nature of str's by using properties without setters.
|
|
38
|
+
ret._group = name
|
|
39
|
+
ret._workbox = sub_name
|
|
40
|
+
return ret
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def group(self):
|
|
44
|
+
"""The tab name of the group tab that contains the workbox."""
|
|
45
|
+
return self._group
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def workbox(self):
|
|
49
|
+
"""The workbox of the tab for this workbox inside of the group."""
|
|
50
|
+
return self._workbox
|
|
51
|
+
|
|
52
|
+
|
|
15
53
|
class WorkboxMixin(object):
|
|
16
54
|
_warning_text = None
|
|
17
55
|
"""When a user is picking this Workbox class, show a warning with this text."""
|
|
@@ -26,6 +64,16 @@ class WorkboxMixin(object):
|
|
|
26
64
|
self._tempfile = tempfile
|
|
27
65
|
self.core_name = core_name
|
|
28
66
|
|
|
67
|
+
self._tab_widget = parent
|
|
68
|
+
|
|
69
|
+
def __tab_widget__(self):
|
|
70
|
+
"""Return the tab widget which contains this workbox
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
GroupedTabWidget: The tab widget which contains this workbox
|
|
74
|
+
"""
|
|
75
|
+
return self._tab_widget
|
|
76
|
+
|
|
29
77
|
def __auto_complete_enabled__(self):
|
|
30
78
|
raise NotImplementedError("Mixin method not overridden.")
|
|
31
79
|
|
|
@@ -70,7 +118,7 @@ class WorkboxMixin(object):
|
|
|
70
118
|
raise NotImplementedError("Mixin method not overridden.")
|
|
71
119
|
|
|
72
120
|
def __exec_selected__(self, truncate=True):
|
|
73
|
-
txt,
|
|
121
|
+
txt, lineNum = self.__selected_text__()
|
|
74
122
|
|
|
75
123
|
# Remove any leading white space shared across all lines
|
|
76
124
|
txt = textwrap.dedent(txt)
|
|
@@ -81,7 +129,7 @@ class WorkboxMixin(object):
|
|
|
81
129
|
# Make workbox line numbers match the workbox line numbers, by adding
|
|
82
130
|
# the appropriate number of newlines to mimic it's original position in
|
|
83
131
|
# the workbox.
|
|
84
|
-
txt = '\n' *
|
|
132
|
+
txt = '\n' * lineNum + txt
|
|
85
133
|
|
|
86
134
|
# execute the code
|
|
87
135
|
title = self.__workbox_trace_title__(selection=True)
|
|
@@ -151,7 +199,57 @@ class WorkboxMixin(object):
|
|
|
151
199
|
if group == -1 or editor == -1:
|
|
152
200
|
return '<{}>'.format(title)
|
|
153
201
|
else:
|
|
154
|
-
|
|
202
|
+
name = self.__workbox_name__()
|
|
203
|
+
return '<{}>:{}'.format(title, name)
|
|
204
|
+
|
|
205
|
+
def __workbox_name__(self, workbox=None):
|
|
206
|
+
"""Returns the name for this workbox or a given workbox.
|
|
207
|
+
The name is the group tab text and the workbox tab text joined by a `/`"""
|
|
208
|
+
workboxTAB = self.window().uiWorkboxTAB
|
|
209
|
+
group_name = None
|
|
210
|
+
workbox_name = None
|
|
211
|
+
|
|
212
|
+
if workbox:
|
|
213
|
+
grouped_tab_widget = workbox.__tab_widget__()
|
|
214
|
+
for group_idx in range(workboxTAB.count()):
|
|
215
|
+
# If a previous iteration determine workbox_name, bust out
|
|
216
|
+
if workbox_name:
|
|
217
|
+
break
|
|
218
|
+
# Check if current group is the workboxes parent group
|
|
219
|
+
cur_group_widget = workboxTAB.widget(group_idx)
|
|
220
|
+
if cur_group_widget == grouped_tab_widget:
|
|
221
|
+
group_name = workboxTAB.tabText(group_idx)
|
|
222
|
+
|
|
223
|
+
# Found the group, now find workbox
|
|
224
|
+
for workbox_idx in range(cur_group_widget.count()):
|
|
225
|
+
cur_workbox_widget = cur_group_widget.widget(workbox_idx)
|
|
226
|
+
if cur_workbox_widget == workbox:
|
|
227
|
+
workbox_name = cur_group_widget.tabText(workbox_idx)
|
|
228
|
+
break
|
|
229
|
+
else:
|
|
230
|
+
grouped = self.__tab_widget__()
|
|
231
|
+
groupedTabBar = grouped.tabBar()
|
|
232
|
+
|
|
233
|
+
idx = -1
|
|
234
|
+
for idx in range(grouped.count()):
|
|
235
|
+
if grouped.widget(idx) == self:
|
|
236
|
+
break
|
|
237
|
+
workbox_name = groupedTabBar.tabText(idx)
|
|
238
|
+
|
|
239
|
+
group = grouped.tab_widget()
|
|
240
|
+
groupTabBar = group.tabBar()
|
|
241
|
+
idx = -1
|
|
242
|
+
for idx in range(group.count()):
|
|
243
|
+
if group.widget(idx) == grouped:
|
|
244
|
+
break
|
|
245
|
+
group_name = groupTabBar.tabText(idx)
|
|
246
|
+
|
|
247
|
+
# If both found, construct workbox name
|
|
248
|
+
if group_name and workbox_name:
|
|
249
|
+
name = WorkboxName(group_name, workbox_name)
|
|
250
|
+
else:
|
|
251
|
+
name = WorkboxName("", "")
|
|
252
|
+
return name
|
|
155
253
|
|
|
156
254
|
def __goto_line__(self, line):
|
|
157
255
|
raise NotImplementedError("Mixin method not overridden.")
|
preditor/gui/workboxwidget.py
CHANGED
|
@@ -207,7 +207,10 @@ class WorkboxWidget(WorkboxMixin, DocumentEditor):
|
|
|
207
207
|
|
|
208
208
|
@classmethod
|
|
209
209
|
def __write_file__(cls, filename, txt, encoding=None):
|
|
210
|
-
# Save unix newlines for simplicity
|
|
210
|
+
# Save unix newlines for simplicity. This should only be called for
|
|
211
|
+
# files which are not linked, so we don't inadvertently change a file's
|
|
212
|
+
# line-endings. For linked files, call saveAs, which bypasses this
|
|
213
|
+
# method writes without converting to unix line endings.
|
|
211
214
|
txt = cls.__unix_end_lines__(txt)
|
|
212
215
|
super(WorkboxWidget, cls).__write_file__(filename, txt, encoding=encoding)
|
|
213
216
|
|
|
@@ -72,9 +72,6 @@ class DocumentEditor(QsciScintilla):
|
|
|
72
72
|
fontsChanged = Signal(
|
|
73
73
|
QFont, QFont
|
|
74
74
|
) # emits the font size change (font size, margin font size)
|
|
75
|
-
documentSaved = Signal(
|
|
76
|
-
QsciScintilla, object
|
|
77
|
-
) # (DocumentEditor, filename) emitted when ever the document is saved.
|
|
78
75
|
|
|
79
76
|
def __init__(self, parent, filename='', lineno=0, delayable_engine='default'):
|
|
80
77
|
super(DocumentEditor, self).__init__(parent)
|
|
@@ -145,7 +142,7 @@ class DocumentEditor(QsciScintilla):
|
|
|
145
142
|
self.customContextMenuRequested.connect(self.showMenu)
|
|
146
143
|
self.selectionChanged.connect(self.updateSelectionInfo)
|
|
147
144
|
window = self.window()
|
|
148
|
-
if hasattr(window, '
|
|
145
|
+
if hasattr(window, 'styleSheetChanged'):
|
|
149
146
|
window.styleSheetChanged.connect(self.updateColorScheme)
|
|
150
147
|
|
|
151
148
|
# Create shortcuts
|
|
@@ -1179,8 +1176,6 @@ class DocumentEditor(QsciScintilla):
|
|
|
1179
1176
|
try:
|
|
1180
1177
|
txt = self.text()
|
|
1181
1178
|
WorkboxMixin.__write_file__(filename, txt, encoding=self._encoding)
|
|
1182
|
-
with open(filename, "w", encoding=self._encoding) as f:
|
|
1183
|
-
f.write(self.text())
|
|
1184
1179
|
except PermissionError as error:
|
|
1185
1180
|
logger.debug('An error occurred while saving')
|
|
1186
1181
|
QMessageBox.question(
|
|
@@ -1191,9 +1186,6 @@ class DocumentEditor(QsciScintilla):
|
|
|
1191
1186
|
)
|
|
1192
1187
|
return False
|
|
1193
1188
|
|
|
1194
|
-
# notify that the document was saved
|
|
1195
|
-
self.documentSaved.emit(self, filename)
|
|
1196
|
-
|
|
1197
1189
|
# update the file
|
|
1198
1190
|
if setFilename:
|
|
1199
1191
|
self.updateFilename(filename)
|
preditor/stream/director.py
CHANGED
|
@@ -6,11 +6,61 @@ import sys
|
|
|
6
6
|
from . import STDERR, STDOUT
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class
|
|
9
|
+
class _DirectorBuffer(io.RawIOBase):
|
|
10
|
+
"""Binary buffer that forwards text writes to the manager.
|
|
11
|
+
|
|
12
|
+
This makes the stream more compatible including if enabled when running tox.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
manager (Manager): The manager that writes are stored in.
|
|
16
|
+
state: The state passed to the manager. This is often ``preditor.stream.STDOUT``
|
|
17
|
+
or ``preditor.stream.STDERR``.
|
|
18
|
+
old_stream: A second stream that will be written to every time this stream
|
|
19
|
+
is written to. This allows this object to replace sys.stdout and still
|
|
20
|
+
send that output to the original stdout, which is useful for not breaking
|
|
21
|
+
DCC's script editors. Pass False to disable this feature. If you pass None
|
|
22
|
+
and state is set to ``preditor.stream.STDOUT`` or ``preditor.stream.STDERR``
|
|
23
|
+
this will automatically be set to the current sys.stdout or sys.stderr.
|
|
24
|
+
name (str, optional): Stored on self.name.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, manager, state, old_stream=None, name='nul'):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.manager = manager
|
|
30
|
+
self.state = state
|
|
31
|
+
self.old_stream = old_stream
|
|
32
|
+
self.name = name
|
|
33
|
+
|
|
34
|
+
def flush(self):
|
|
35
|
+
if self.old_stream:
|
|
36
|
+
self.old_stream.flush()
|
|
37
|
+
super().flush()
|
|
38
|
+
|
|
39
|
+
def writable(self):
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
def write(self, b):
|
|
43
|
+
if isinstance(b, memoryview):
|
|
44
|
+
b = b.tobytes()
|
|
45
|
+
|
|
46
|
+
# Decode incoming bytes (TextIOWrapper encodes before sending here)
|
|
47
|
+
msg = b.decode("utf-8", errors="replace")
|
|
48
|
+
self.manager.write(msg, self.state)
|
|
49
|
+
|
|
50
|
+
if self.old_stream:
|
|
51
|
+
self.old_stream.write(msg)
|
|
52
|
+
|
|
53
|
+
return len(b)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Director(io.TextIOWrapper):
|
|
10
57
|
"""A file like object that stores the text written to it in a manager.
|
|
11
58
|
This manager can be shared between multiple Directors to build a single
|
|
12
59
|
continuous history of all writes.
|
|
13
60
|
|
|
61
|
+
While this uses a buffer under the hood, buffering is disabled and any calls
|
|
62
|
+
to write will automatically flush the buffer.
|
|
63
|
+
|
|
14
64
|
Args:
|
|
15
65
|
manager (Manager): The manager that writes are stored in.
|
|
16
66
|
state: The state passed to the manager. This is often ``preditor.stream.STDOUT``
|
|
@@ -24,14 +74,11 @@ class Director(io.TextIOBase):
|
|
|
24
74
|
"""
|
|
25
75
|
|
|
26
76
|
def __init__(self, manager, state, old_stream=None, *args, **kwargs):
|
|
27
|
-
super(Director, self).__init__(*args, **kwargs)
|
|
28
|
-
self.manager = manager
|
|
29
|
-
self.state = state
|
|
30
|
-
|
|
31
77
|
# Keep track of whether we wrapped a std stream
|
|
32
78
|
# that way we don't .close() any streams that we don't control
|
|
33
79
|
self.std_stream_wrapped = False
|
|
34
80
|
|
|
81
|
+
name = 'nul'
|
|
35
82
|
if old_stream is False:
|
|
36
83
|
old_stream = None
|
|
37
84
|
elif old_stream is None:
|
|
@@ -39,15 +86,28 @@ class Director(io.TextIOBase):
|
|
|
39
86
|
# On Windows if we're in pythonw.exe, then sys.stdout is named "nul"
|
|
40
87
|
# And it uses cp1252 encoding (which breaks with unicode)
|
|
41
88
|
# So if we find this nul TextIOWrapper, it's safe to just skip it
|
|
42
|
-
|
|
89
|
+
name = getattr(sys.stdout, 'name', '')
|
|
90
|
+
if name != 'nul':
|
|
43
91
|
self.std_stream_wrapped = True
|
|
44
92
|
old_stream = sys.stdout
|
|
45
93
|
elif state == STDERR:
|
|
46
|
-
|
|
94
|
+
name = getattr(sys.stderr, 'name', '')
|
|
95
|
+
if name != 'nul':
|
|
47
96
|
self.std_stream_wrapped = True
|
|
48
97
|
old_stream = sys.stderr
|
|
49
98
|
|
|
50
99
|
self.old_stream = old_stream
|
|
100
|
+
self.manager = manager
|
|
101
|
+
self.state = state
|
|
102
|
+
|
|
103
|
+
# Build the buffer. This provides the expected interface for tox, etc.
|
|
104
|
+
raw = _DirectorBuffer(manager, state, old_stream, name)
|
|
105
|
+
buffer = io.BufferedWriter(raw)
|
|
106
|
+
|
|
107
|
+
super().__init__(buffer, encoding="utf-8", write_through=True, *args, **kwargs)
|
|
108
|
+
|
|
109
|
+
def __repr__(self):
|
|
110
|
+
return f"<Director state={self.state} old_stream={self.old_stream!r}>"
|
|
51
111
|
|
|
52
112
|
def close(self):
|
|
53
113
|
if (
|
|
@@ -58,16 +118,27 @@ class Director(io.TextIOBase):
|
|
|
58
118
|
):
|
|
59
119
|
self.old_stream.close()
|
|
60
120
|
|
|
61
|
-
super(
|
|
121
|
+
super().close()
|
|
62
122
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
123
|
+
def write(self, msg):
|
|
124
|
+
super().write(msg)
|
|
125
|
+
# Force a write of any buffered data
|
|
126
|
+
self.flush()
|
|
66
127
|
|
|
67
|
-
|
|
128
|
+
# These methods enable terminal features like color coding etc.
|
|
129
|
+
def isatty(self):
|
|
130
|
+
if self.old_stream is not None:
|
|
131
|
+
return self.old_stream.isatty()
|
|
132
|
+
return False
|
|
68
133
|
|
|
69
|
-
|
|
70
|
-
|
|
134
|
+
@property
|
|
135
|
+
def encoding(self):
|
|
136
|
+
if self.old_stream is not None:
|
|
137
|
+
return self.old_stream.encoding
|
|
138
|
+
return super().encoding
|
|
71
139
|
|
|
72
|
-
|
|
73
|
-
|
|
140
|
+
@property
|
|
141
|
+
def errors(self):
|
|
142
|
+
if self.old_stream is not None:
|
|
143
|
+
return self.old_stream.errors
|
|
144
|
+
return super().errors
|
preditor/version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.5.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 5, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gb7045f3af'
|
|
@@ -4,7 +4,7 @@ preditor/about_module.py,sha256=_jhd2vaWArnnw2qvsZtOSQvntec9PG_PCiSFbcS1j_I,5347
|
|
|
4
4
|
preditor/cli.py,sha256=kyVr0V43KYSMLIEWXH54CA0ByFmPcpbFF-8cli6PVow,5300
|
|
5
5
|
preditor/config.py,sha256=DTDqJ8cVnbvAE8IytAREMV7HJS2b3DLH1jtFIkdTo8c,11883
|
|
6
6
|
preditor/contexts.py,sha256=xxRbOFORsvlG___EuSiKuT1aiKJvnkFojFZFZE0HPNk,5147
|
|
7
|
-
preditor/debug.py,sha256=
|
|
7
|
+
preditor/debug.py,sha256=9BkNis3l4gockQJpblxe6jnh6OyMITKzY3WKd035vGI,4625
|
|
8
8
|
preditor/enum.py,sha256=snG5V259dH9SI1kwBqZeZdhat36F1iAmIjYErosnDUg,23489
|
|
9
9
|
preditor/excepthooks.py,sha256=UZ2PsxUo8hA_PBabAXsQkiwKgTLgY_3lueFymIb0V50,5127
|
|
10
10
|
preditor/logging_config.py,sha256=bGRCaSq6ugPG2L5keTpw5CNiQwi9PFDxDMDhx_VLHsg,1583
|
|
@@ -13,7 +13,7 @@ preditor/plugins.py,sha256=RAeyvdZxQsZY313ae5aAhahqpjuhaXJzDxPyLcR-09U,4401
|
|
|
13
13
|
preditor/prefs.py,sha256=BPtSsdv2yuiRpIaqEml9fxlVYKHNfqQ77hp5YIQRDBg,2172
|
|
14
14
|
preditor/settings.py,sha256=DV9_DbJorEnhdIvW15E7h7PswlQUsy0UlA8bXUYN0og,2206
|
|
15
15
|
preditor/streamhandler_helper.py,sha256=kiU6T9WqJ3JKTTKCa7IUU8brwK7zO5UUpEzLhEfKe44,1788
|
|
16
|
-
preditor/version.py,sha256=
|
|
16
|
+
preditor/version.py,sha256=9Si62UWsEQhvgE6BKzBJWtlwXjMhsCJF_E0gnxJgjEU,712
|
|
17
17
|
preditor/weakref.py,sha256=b--KomFAHcMWr3DEAIN2j3XxRhjDWKw0WABXyn1nxDg,12177
|
|
18
18
|
preditor/cores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
preditor/cores/core.py,sha256=mdVXlEPDh8kIdOpMDqi8P1VaqfVvyu5cXBq_toONcaM,586
|
|
@@ -32,31 +32,31 @@ preditor/gui/__init__.py,sha256=DeOH5K4_M0YLNx1i7NRvwCgwjyjkre4vVwlr5OEMx9Q,3423
|
|
|
32
32
|
preditor/gui/app.py,sha256=EhVsVMr5C5WSKNDlxGawzg-3lbTJxDMLi2fPomECacM,5869
|
|
33
33
|
preditor/gui/codehighlighter.py,sha256=wUOgLDhcAQf72DSS65r9_RYdv6PVJwWFXTdOPvvps_A,6790
|
|
34
34
|
preditor/gui/completer.py,sha256=U2FBJA_ceFq_viGqPogncH-6vhrtTcpxAF61SUnaOJ8,7813
|
|
35
|
-
preditor/gui/console.py,sha256=
|
|
35
|
+
preditor/gui/console.py,sha256=It-cXB91kwtbmkkkTwZ78fi3jt4SnHQDId5GmsyULtY,40765
|
|
36
36
|
preditor/gui/dialog.py,sha256=fasmBDhN-B6uretbSQ3x1SxImsQLSKIMw4PC9B8BimI,6535
|
|
37
|
-
preditor/gui/drag_tab_bar.py,sha256=
|
|
37
|
+
preditor/gui/drag_tab_bar.py,sha256=EgEXdV4odNj_1_YiGP2kmpGKUaBPe75JTwudU8Xj-Gw,8113
|
|
38
38
|
preditor/gui/editor_chooser.py,sha256=lv1eY0UJOulX1l-P-ZQEoneYz6BNX2VkXEbg3GUu1ag,1991
|
|
39
39
|
preditor/gui/errordialog.py,sha256=cRtQ-l40kvQ9awRDqAjetwT8S4oD1iztPbEEly7GYBU,2075
|
|
40
40
|
preditor/gui/find_files.py,sha256=TtsjHXKWvwKrxHYZryB_NPANoFpMRlAxWJp1hA6Ny2I,4360
|
|
41
41
|
preditor/gui/level_buttons.py,sha256=h0pMMzp-ElwcN_qfR4EBQtXn1C666KVcnR9dMoixD3E,12597
|
|
42
42
|
preditor/gui/logger_window_handler.py,sha256=43-DxzuNJq6UV6Ofc4TKRt179Sf5AxDkVDGrtq_Knok,1571
|
|
43
|
-
preditor/gui/logger_window_plugin.py,sha256=
|
|
44
|
-
preditor/gui/loggerwindow.py,sha256=
|
|
43
|
+
preditor/gui/logger_window_plugin.py,sha256=Lj9Q8L82GjzZ21ZI5W0FCHhrPHNALIunT_C9ua15l2w,1272
|
|
44
|
+
preditor/gui/loggerwindow.py,sha256=PaTTa_vTXDAjEWxwPBEtTosGppu9S38wI3zOUy7MGSI,54458
|
|
45
45
|
preditor/gui/newtabwidget.py,sha256=5aCWn9xAl5h1oZACqVuEsOAbzKTS2RegrLI41gROC8A,1971
|
|
46
46
|
preditor/gui/set_text_editor_path_dialog.py,sha256=bn8IeQHXPSU4PLYXQmSxKB1V8iuJaI1PA5aDJNgxXks,2292
|
|
47
47
|
preditor/gui/status_label.py,sha256=SlNRmPc28S4E2OFVmErv0DmZstk4011Q_7uLp43SF0A,3320
|
|
48
48
|
preditor/gui/suggest_path_quotes_dialog.py,sha256=QUJf_9hs8wOO6bFOr8_Z2xnNhSA8TfKFMOzheUqUDnY,1822
|
|
49
49
|
preditor/gui/window.py,sha256=6Ztjqb0Xfh8DsI7hBF4Rg-4-gWPZKTV4GRJiz3V8O6Q,5904
|
|
50
|
-
preditor/gui/workbox_mixin.py,sha256=
|
|
50
|
+
preditor/gui/workbox_mixin.py,sha256=_XrivnyBOjnjHE35WrDXGJbwxP0vFkC2avhypLOHd_U,18562
|
|
51
51
|
preditor/gui/workbox_text_edit.py,sha256=wrJcGqIJ2-5BqEcP_qVKNlWUD8Geo8IxrIXncnDG6yw,4472
|
|
52
|
-
preditor/gui/workboxwidget.py,sha256
|
|
52
|
+
preditor/gui/workboxwidget.py,sha256=-u4DiwgJ6Oc_WjeWSny5HKu4KfJY7uQ0wUftnuH9R7Q,10840
|
|
53
53
|
preditor/gui/fuzzy_search/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
54
|
preditor/gui/fuzzy_search/fuzzy_search.py,sha256=zohr6oAE99FZFlLjqKZajBrdyJIYLW96rcCOy5Bn1i0,3714
|
|
55
|
-
preditor/gui/group_tab_widget/__init__.py,sha256=
|
|
55
|
+
preditor/gui/group_tab_widget/__init__.py,sha256=EalGlpDcinbI6WdwmG7BK94519rUHfzxA0qez-o4m6k,13637
|
|
56
56
|
preditor/gui/group_tab_widget/grouped_tab_menu.py,sha256=lopd3mjHJIE_5xLdlZL0ghuBzsYo36vO1OZE6L6tqbI,1288
|
|
57
57
|
preditor/gui/group_tab_widget/grouped_tab_models.py,sha256=duwEdtbvsJ1nPKFakrnY8QuCoBx4M7mOpB0_0Nrm6_Y,4027
|
|
58
|
-
preditor/gui/group_tab_widget/grouped_tab_widget.py,sha256=
|
|
59
|
-
preditor/gui/group_tab_widget/one_tab_widget.py,sha256=
|
|
58
|
+
preditor/gui/group_tab_widget/grouped_tab_widget.py,sha256=zIKr2oh_TgGokWmAFDIeyw6touH6vRo3N1MCnOyJQRM,3624
|
|
59
|
+
preditor/gui/group_tab_widget/one_tab_widget.py,sha256=XHIgZwO4g0jtY6rJExB7389vhuLJeKg9HwZnDM1V66k,3383
|
|
60
60
|
preditor/gui/ui/editor_chooser.ui,sha256=cYSVHK0A4-zst6JyDkrBZK9qcAontbhV4Mmnw5ps72E,2736
|
|
61
61
|
preditor/gui/ui/errordialog.ui,sha256=i7cVc28FPxA_b4Xa58W_xmRJ_1GFb5rgtUhR_gZcf4E,1831
|
|
62
62
|
preditor/gui/ui/find_files.ui,sha256=8mdD3Vg3ofRMChdKntxiDHO3JXHQSKjjY6OLY_1W5lc,3328
|
|
@@ -116,7 +116,7 @@ preditor/resource/lang/python.json,sha256=CXiQh0jcgd-OCrM-s9IF7s4o-g5WRA4vDaAyTR
|
|
|
116
116
|
preditor/resource/stylesheet/Bright.css,sha256=SfnPRBqfqEL4IF8RGt9PpLX_cSiGpfG9Q2hE1F9uTsk,2480
|
|
117
117
|
preditor/resource/stylesheet/Dark.css,sha256=Sige7beBhRyfav9_mXvn-LhksxbX0_WEgpZSKhmPavc,5384
|
|
118
118
|
preditor/scintilla/__init__.py,sha256=IDndbcS8KW1TMcQTA1Vd-LzcdsNtOUfyldnjkKae0yw,956
|
|
119
|
-
preditor/scintilla/documenteditor.py,sha256=
|
|
119
|
+
preditor/scintilla/documenteditor.py,sha256=sLdVtRsq63LZEd4_Bmpn5JEAmTpBgFG_M4MZUxfz6yA,81129
|
|
120
120
|
preditor/scintilla/finddialog.py,sha256=WxNVvkCfXrw_8E3-7tS1DIS9H1-ioLASDgIbniDxz3g,2376
|
|
121
121
|
preditor/scintilla/delayables/__init__.py,sha256=lj9tMc3IL2QeaN858Ixt_n7clJngbKqG2sk66vIcFcQ,275
|
|
122
122
|
preditor/scintilla/delayables/smart_highlight.py,sha256=5emI4StsQCIYizoFsib1rdw0gnUarV9TQ6CwxmzXXpU,3623
|
|
@@ -151,17 +151,17 @@ preditor/scintilla/lexers/mulexer.py,sha256=tlyiKYBDRpmxcrH9FGi-Ogz2alHPhyqNaqVr
|
|
|
151
151
|
preditor/scintilla/lexers/pythonlexer.py,sha256=dh_wSBhhYIIV1hUF_0cWbZCdMBMsOB6EY6x8valWo1s,1737
|
|
152
152
|
preditor/scintilla/ui/finddialog.ui,sha256=CEXouVKqVCDOfMBciK-89OmsGe2Kr6XdMbSPiffp0PA,4048
|
|
153
153
|
preditor/stream/__init__.py,sha256=SxILA3U3W8aNfVBd_oJ4-WXkA214cl1zGLSCelFx5QU,2682
|
|
154
|
-
preditor/stream/director.py,sha256=
|
|
154
|
+
preditor/stream/director.py,sha256=UIGhLoZPCMHlDT04CPbrm4aTzggd4A9wR2Fitu2LT08,5296
|
|
155
155
|
preditor/stream/manager.py,sha256=NP4lf6hd5L_Ui-9Ws66gVEk6ZL8YqF7BxOzUsdrh3v0,2811
|
|
156
156
|
preditor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
157
157
|
preditor/utils/cute.py,sha256=tLTChQ1-zMzHHaKpG85u4FbtJl_2R1E8-uckpOQbEEM,1057
|
|
158
158
|
preditor/utils/stylesheets.py,sha256=EVWZNq3WnaRiyUPoYMKQo_dLEwbRyKu26b03I1JDA-s,1622
|
|
159
159
|
preditor/utils/text_search.py,sha256=21kuSDTpLIPUcriB81WP1kWfzuDBuP13ZtHUtypP5jE,14218
|
|
160
|
-
preditor-1.
|
|
160
|
+
preditor-1.5.0.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
|
|
161
161
|
tests/find_files/test_find_files.py,sha256=ETVe1tC666uTc_5pPhjzPCaMrMkYUB8iKGXDfW9usgI,2722
|
|
162
162
|
tests/ide/test_delayable_engine.py,sha256=Nv6SQXpwIdfgRbEOsEcR-7jhjZ6E0THPdwDoPvAM6y4,5948
|
|
163
|
-
preditor-1.
|
|
164
|
-
preditor-1.
|
|
165
|
-
preditor-1.
|
|
166
|
-
preditor-1.
|
|
167
|
-
preditor-1.
|
|
163
|
+
preditor-1.5.0.dist-info/METADATA,sha256=GsCQMiLBp5l5PPuC3pq34mqNgg_DFvlIA2f6fAwWQfI,12744
|
|
164
|
+
preditor-1.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
165
|
+
preditor-1.5.0.dist-info/entry_points.txt,sha256=mpe0HFD_oIEBNPTJNyUEbmMV6Ivrp4EuYyZ34C5d_PU,519
|
|
166
|
+
preditor-1.5.0.dist-info/top_level.txt,sha256=dZSBDecBQovRyqbFdvwk1AvMon636dJ14vr9ov1LSPs,20
|
|
167
|
+
preditor-1.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|