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,223 @@
1
+ from __future__ import absolute_import
2
+
3
+ from Qt.QtCore import Qt
4
+ from Qt.QtWidgets import QMessageBox, QToolButton
5
+
6
+ from ...prefs import VersionTypes
7
+ from ..drag_tab_bar import DragTabBar
8
+ from ..workbox_text_edit import WorkboxTextEdit
9
+ from .one_tab_widget import OneTabWidget
10
+
11
+
12
+ class GroupedTabWidget(OneTabWidget):
13
+ def __init__(self, editor_kwargs, editor_cls=None, core_name=None, *args, **kwargs):
14
+ super(GroupedTabWidget, self).__init__(*args, **kwargs)
15
+ DragTabBar.install_tab_widget(self, 'grouped_tab_widget')
16
+ self.editor_kwargs = editor_kwargs
17
+ if editor_cls is None:
18
+ editor_cls = WorkboxTextEdit
19
+ self.editor_cls = editor_cls
20
+ self.core_name = core_name
21
+ self.currentChanged.connect(self.tab_shown)
22
+
23
+ self.uiCornerBTN = QToolButton(self)
24
+ self.uiCornerBTN.setText('+')
25
+ self.uiCornerBTN.released.connect(lambda: self.add_new_editor())
26
+ self.setCornerWidget(self.uiCornerBTN, Qt.Corner.TopRightCorner)
27
+
28
+ self.default_title = "Workbox01"
29
+
30
+ def __tab_widget__(self):
31
+ """Return the tab widget which contains this group
32
+
33
+ Returns:
34
+ GroupTabWidget: The tab widget which contains this group
35
+ """
36
+ return self.parent().parent()
37
+
38
+ def __changed_by_instance__(self):
39
+ """Returns if any of this groups editors have been changed by another
40
+ PrEditor instance's prefs save.
41
+
42
+ Returns:
43
+ changed (bool)
44
+ """
45
+ changed = False
46
+ for workbox_idx in range(self.count()):
47
+ workbox = self.widget(workbox_idx)
48
+ if workbox.__changed_by_instance__():
49
+ changed = True
50
+ break
51
+ return changed
52
+
53
+ def __orphaned_by_instance__(self):
54
+ """Returns if any of this groups editors have been orphaned by another
55
+ PrEditor instance's prefs save.
56
+
57
+ Returns:
58
+ orphaned (bool)
59
+ """
60
+ orphaned = False
61
+ for workbox_idx in range(self.count()):
62
+ workbox = self.widget(workbox_idx)
63
+ if workbox.__orphaned_by_instance__():
64
+ orphaned = True
65
+ break
66
+ return orphaned
67
+
68
+ def __is_dirty__(self):
69
+ """Returns if any of this groups editors are dirty.
70
+
71
+ Returns:
72
+ is_dirty (bool)
73
+ """
74
+ is_dirty = False
75
+ for workbox_idx in range(self.count()):
76
+ workbox = self.widget(workbox_idx)
77
+ if workbox.__is_dirty__():
78
+ is_dirty = True
79
+ break
80
+ return is_dirty
81
+
82
+ def __is_missing_linked_file__(self):
83
+ """Determine if any of the workboxes are linked to file which is missing
84
+ on disk.
85
+
86
+ Returns:
87
+ bool: Whether any of this group's workboxes define a linked file
88
+ which is missing on disk.
89
+ """
90
+ is_missing_linked_file = False
91
+ for workbox_idx in range(self.count()):
92
+ workbox = self.widget(workbox_idx)
93
+ if workbox.__is_missing_linked_file__():
94
+ is_missing_linked_file = True
95
+ break
96
+ return is_missing_linked_file
97
+
98
+ def add_new_editor(self, title=None, prefs=None):
99
+ title = title or self.default_title
100
+
101
+ title = self.get_next_available_tab_name(title)
102
+ editor, title = self.default_tab(title, prefs=prefs)
103
+ index = self.addTab(editor, title)
104
+ self.setCurrentIndex(index)
105
+ return editor
106
+
107
+ def addTab(self, *args, **kwargs): # noqa: N802
108
+ ret = super(GroupedTabWidget, self).addTab(*args, **kwargs)
109
+ self.update_closable_tabs()
110
+ return ret
111
+
112
+ def close_tab(self, index, ask=True):
113
+ if self.count() == 1:
114
+ msg = "You have to leave at least one tab open."
115
+ QMessageBox.critical(
116
+ self, 'Tab can not be closed.', msg, QMessageBox.StandardButton.Ok
117
+ )
118
+ return
119
+
120
+ workbox = self.widget(index)
121
+ name = workbox.__workbox_name__()
122
+ msg = (
123
+ f"Would you like to donate the contents of tab\n{name}\nto the "
124
+ "/dev/null fund for wayward code?"
125
+ )
126
+
127
+ if ask:
128
+ ret = QMessageBox.question(
129
+ self,
130
+ 'Donate to the cause?',
131
+ msg,
132
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
133
+ )
134
+ else:
135
+ ret = QMessageBox.StandardButton.Yes
136
+
137
+ if ret == QMessageBox.StandardButton.Yes:
138
+ editor = self.widget(index)
139
+ editor.__set_file_monitoring_enabled__(False)
140
+ self.store_closed_workbox(index)
141
+ super(GroupedTabWidget, self).close_tab(index)
142
+
143
+ def store_closed_workbox(self, index):
144
+ """Store the name of the workbox being closed.
145
+
146
+ Args:
147
+ index (int): The index of the workbox being closed
148
+ """
149
+ workbox = self.widget(index)
150
+
151
+ # Save the workbox first, so we can possibly restore it later.
152
+ workbox.__save_prefs__(saveLinkedFile=False)
153
+
154
+ self.parent().window().addRecentlyClosedWorkbox(workbox)
155
+
156
+ def default_tab(self, title=None, prefs=None):
157
+ title = title or self.default_title
158
+ kwargs = self.editor_kwargs if self.editor_kwargs else {}
159
+ editor = None
160
+ orphaned_by_instance = False
161
+ if prefs:
162
+ editor_info = prefs.pop("existing_editor_info", None)
163
+ if editor_info:
164
+ editor = editor_info[0]
165
+ orphaned_by_instance = prefs.pop("orphaned_by_instance", False)
166
+ else:
167
+ prefs = {}
168
+
169
+ if editor:
170
+ editor.__load_workbox_version_text__(VersionTypes.Last)
171
+
172
+ editor.__set_last_saved_text__(editor.__text__())
173
+ editor.__set_last_workbox_name__(editor.__workbox_name__())
174
+
175
+ filename = prefs.get("filename", None)
176
+ editor.__set_filename__(filename)
177
+
178
+ editor.__determine_been_changed_by_instance__()
179
+ self.window().setWorkboxFontBasedOnConsole(editor)
180
+ else:
181
+ editor = self.editor_cls(
182
+ parent=self, core_name=self.core_name, **prefs, **kwargs
183
+ )
184
+ editor.__set_orphaned_by_instance__(orphaned_by_instance)
185
+ return editor, title
186
+
187
+ def get_next_available_tab_name(self, name=None):
188
+ """Get the next available tab name, providing a default if needed.
189
+
190
+ Args:
191
+ name (str, optional): The name for which to get the next available
192
+ name.
193
+
194
+ Returns:
195
+ str: The determined next available tab name
196
+ """
197
+ if name is None:
198
+ name = self.default_title
199
+ return super().get_next_available_tab_name(name)
200
+
201
+ def showEvent(self, event): # noqa: N802
202
+ super(GroupedTabWidget, self).showEvent(event)
203
+ self.tab_shown(self.currentIndex())
204
+
205
+ def tab_shown(self, index):
206
+ editor = self.widget(index)
207
+ if editor and editor.isVisible():
208
+ editor.__show__()
209
+
210
+ if hasattr(self.window(), "setWorkboxFontBasedOnConsole"):
211
+ self.window().setWorkboxFontBasedOnConsole()
212
+
213
+ def tab_widget(self):
214
+ """Return the tab widget which contains this group tab
215
+
216
+ Returns:
217
+ self._tab_widget (GroupTabWidget): The tab widget which contains
218
+ this workbox
219
+ """
220
+ return self.parent().parent()
221
+
222
+ def update_closable_tabs(self):
223
+ self.setTabsClosable(self.count() != 1)
@@ -0,0 +1,96 @@
1
+ from __future__ import absolute_import
2
+
3
+ import re
4
+
5
+ from Qt.QtWidgets import QTabWidget
6
+
7
+ TAB_ITERATION_PATTERN = re.compile(r"(\d+)(?!.*\d)")
8
+
9
+
10
+ class OneTabWidget(QTabWidget):
11
+ """A QTabWidget that shows the close button only if there is more than one
12
+ tab. If something removes the last tab, it will add a default tab if the
13
+ default_tab method is implemented on a subclass. This is also used to create
14
+ the first tab on showEvent.
15
+
16
+ Subclasses can implement a `default_tab()` method. This should return the
17
+ widget to add and the title of the tab to create if implemented.
18
+ """
19
+
20
+ def __init__(self, *args, **kwargs):
21
+ super(OneTabWidget, self).__init__(*args, **kwargs)
22
+ self.tabCloseRequested.connect(self.close_tab)
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
+ existing_names = [self.tabText(i) for i in range(self.count())]
34
+
35
+ # Use regex to find the last set of digits. If found, the base name is
36
+ # a slice of name minus the digits string. Otherwise, the base name is
37
+ # the full name and iteration is zero.
38
+ match = TAB_ITERATION_PATTERN.search(name)
39
+ if match:
40
+ # We found trailing digits, so slice to get base name, and convert
41
+ # iteration to int
42
+ iter_str = match.group()
43
+ base = name[: -len(iter_str)]
44
+ iteration = int(iter_str)
45
+ else:
46
+ # No trailing digits found, so base name is full name and iteration
47
+ # is zero.
48
+ base = name
49
+ iteration = 0
50
+
51
+ if name in existing_names:
52
+ for _ in range(99):
53
+ iteration += 1
54
+ new_iter_str = str(iteration).zfill(2)
55
+ name = base + new_iter_str
56
+ if name not in existing_names:
57
+ break
58
+ return name
59
+
60
+ def addTab(self, *args, **kwargs): # noqa: N802
61
+ ret = super(OneTabWidget, self).addTab(*args, **kwargs)
62
+ self.update_closable_tabs()
63
+ self.tabBar().setFont(self.window().font())
64
+ return ret
65
+
66
+ def close_tab(self, index):
67
+ self.removeTab(index)
68
+ self.update_closable_tabs()
69
+
70
+ def index_for_text(self, text):
71
+ """Return the index of the tab with this text. Returns -1 if not found"""
72
+ for i in range(self.count()):
73
+ if self.tabText(i) == text:
74
+ return i
75
+ return -1
76
+
77
+ def insertTab(self, *args, **kwargs): # noqa: N802
78
+ ret = super(OneTabWidget, self).insertTab(*args, **kwargs)
79
+ self.update_closable_tabs()
80
+ return ret
81
+
82
+ def removeTab(self, index): # noqa: N802
83
+ super(OneTabWidget, self).removeTab(index)
84
+ if hasattr(self, 'default_tab') and not self.count():
85
+ self.addTab(*self.default_tab())
86
+ self.update_closable_tabs()
87
+ self.window().updateTabColorsAndToolTips()
88
+
89
+ def showEvent(self, event): # noqa: N802
90
+ super(OneTabWidget, self).showEvent(event)
91
+ # Force the creation of a default tab if defined
92
+ if hasattr(self, 'default_tab') and not self.count():
93
+ self.addTab(*self.default_tab())
94
+
95
+ def update_closable_tabs(self):
96
+ self.setTabsClosable(self.count() != 1)
@@ -0,0 +1,358 @@
1
+ from __future__ import absolute_import
2
+
3
+ import logging
4
+ import types
5
+ from functools import partial
6
+
7
+ from Qt.QtGui import QIcon
8
+ from Qt.QtWidgets import QAction, QMenu, QToolButton
9
+
10
+ from .. import plugins, resourcePath
11
+ from ..enum import Enum, EnumGroup
12
+
13
+
14
+ class Level(Enum):
15
+
16
+ """
17
+ Custom `Enum` representing an information level.
18
+
19
+ Attributes:
20
+ cached_icon(None): Used to cache the created icon from `get_icon` for
21
+ future use.
22
+ icon_name(str): Name of source icon file to use when creating icon via
23
+ `get_icon`.
24
+ """
25
+
26
+ cached_icon = None
27
+ icon_name = "dot"
28
+
29
+ @property
30
+ def name(self):
31
+ """
32
+ Override of `name` property allowing for the return of a "friendly
33
+ name" to be used in place of the inferred name from the `Enum` instance
34
+ name.
35
+
36
+ Returns:
37
+ str: Name of `Enum` instance.
38
+ """
39
+ return getattr(self, "friendly_name", super(Level, self).name)
40
+
41
+ @property
42
+ def icon(self):
43
+ """
44
+ Icon representing the level. On first access, the icon is created via
45
+ the `get_icon`-method and cached for later use.
46
+
47
+ Returns:
48
+ QIcon:
49
+ """
50
+ if not self.cached_icon:
51
+ self.cached_icon = self.get_icon(self.icon_name, self.level)
52
+ return self.cached_icon
53
+
54
+ def get_icon(self, name, level):
55
+ """
56
+ Retrieves the icon of `name` and level.
57
+
58
+ Args:
59
+ name (str): Icon to retrieve QIcon for.
60
+ level (str): Level name to apply.
61
+
62
+ Returns:
63
+ QIcon: Correct instantiated QIcon.
64
+ """
65
+ return QIcon(
66
+ resourcePath('img/{name}_{level}.png'.format(name=name, level=level))
67
+ )
68
+
69
+
70
+ class LoggerLevel(Level):
71
+ """A Logger level `Enum` using the 'format_align_left' icon."""
72
+
73
+ icon_name = "logging"
74
+
75
+ def is_current(self, logger):
76
+ """Returns if the current logging level matches this label."""
77
+ return logging.getLevelName(logger.level) == self.label
78
+
79
+
80
+ class LoggerLevels(EnumGroup):
81
+ """
82
+ Logger levels with their implementation level name and number & custom
83
+ icon level.
84
+ """
85
+
86
+ Disabled = LoggerLevel(
87
+ friendly_name="Not Set / Inherited", label="NOTSET", number=0, level="not_set"
88
+ )
89
+ Critical = LoggerLevel(label="CRITICAL", number=50, level="critical")
90
+ Error = LoggerLevel(label="ERROR", number=40, level="error")
91
+ Warning = LoggerLevel(label="WARNING", number=30, level="warning")
92
+ Info = LoggerLevel(label="INFO", number=20, level="info")
93
+ Debug = LoggerLevel(label="DEBUG", number=10, level="debug")
94
+
95
+ @classmethod
96
+ def fromLabel(cls, label, default=None, logger=None):
97
+ try:
98
+ return super(LoggerLevels, cls).fromLabel(label, default=default)
99
+ except ValueError:
100
+ # This is not be a standard level, generate a custom level to use
101
+ if logger is None:
102
+ logger = logging.getLogger()
103
+ effective_level = logger.getEffectiveLevel()
104
+ effective_level_name = logging.getLevelName(effective_level)
105
+
106
+ enum = LoggerLevel(
107
+ label=effective_level_name,
108
+ number=effective_level,
109
+ level=effective_level_name,
110
+ )
111
+ # Force the custom icon as this enum's name won't match
112
+ enum.cached_icon = enum.get_icon(enum.icon_name, "custom")
113
+ # Add it to the enum
114
+ LoggerLevels.append(enum)
115
+ return enum
116
+
117
+
118
+ class LoggingLevelButton(QToolButton):
119
+
120
+ """
121
+ A drop down button to set logger levels for all loggers known to Python's
122
+ native logging implementation.
123
+
124
+ The logger menus present in the tool bar button have level-changing actions
125
+ as well a sub-menus for any descendant loggers.
126
+ """
127
+
128
+ def __init__(self, parent=None):
129
+ """
130
+ Creates the root logger menu this button displays when clicked.
131
+ Additionally, any pre-existing loggers and their menus are added.
132
+
133
+ Args:
134
+ parent (QWidget, optional): The parent widget for this button.
135
+ """
136
+ super(LoggingLevelButton, self).__init__(parent=parent)
137
+ self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
138
+
139
+ # create root logger menu
140
+ root = logging.getLogger("")
141
+ root_menu = LoggingLevelMenu(name="root", logger=root, parent=self)
142
+ self.setMenu(root_menu)
143
+
144
+ # TODO: Hook refresh up to a root logger signal
145
+ # Monkey patch root.setLogger to emit signal we connect to
146
+ root = self.patched_root_logger().level_changed.connect(self.refresh)
147
+
148
+ @staticmethod
149
+ def patched_root_logger():
150
+ """Returns `logging.getLogger("")`. This will have the level_changed
151
+ signal added if it wasn't already.
152
+
153
+ The level_changed signal is emitted any time something changes the
154
+ root logger level. PrEditor uses this to update the logging level button
155
+ icon any time the root logger's level is changed. The rest of the loggers
156
+ don't need this as the menu is built on demand with the correct icons indicated.
157
+ """
158
+ root = logging.getLogger("")
159
+ if hasattr(root, "level_changed"):
160
+ # Already patched, nothing to do
161
+ return root
162
+
163
+ # Need to patch the root logger
164
+ from signalslot import Signal
165
+
166
+ root.level_changed = Signal(args=["level"], name="level_changed")
167
+
168
+ # Store the current setLevel, so we can call it in our method
169
+ root._setLevel = root.setLevel
170
+
171
+ def setLevel(self, level):
172
+ """
173
+ Sets the threshold for this logger to `level`. Also emits the
174
+ instance's `level_changed`-signal with the level number as its payload.
175
+
176
+ Args:
177
+ level (int): Numeric level value.
178
+ """
179
+ # Call the original setLevel method
180
+ self._setLevel(level)
181
+ # Emit our signal
182
+ self.level_changed.emit(level=level)
183
+
184
+ root.setLevel = types.MethodType(setLevel, root)
185
+
186
+ return root
187
+
188
+ def refresh(self, **kwargs):
189
+ effective_level = logging.getLogger("").getEffectiveLevel()
190
+ effective_level_name = logging.getLevelName(effective_level)
191
+ level_enum = LoggerLevels.fromLabel(effective_level_name)
192
+
193
+ self.setIcon(level_enum.icon)
194
+ self.setToolTip("Logger 'root' current level: {}".format(level_enum.name))
195
+
196
+
197
+ class LazyMenu(QMenu):
198
+ """A menu class that only calls self.refresh when it is about to be shown."""
199
+
200
+ def __init__(self, *args, **kwargs):
201
+ super(LazyMenu, self).__init__(*args, **kwargs)
202
+ self.aboutToShow.connect(self.refresh)
203
+
204
+
205
+ class HandlerMenu(LazyMenu):
206
+ def __init__(self, logger, parent=None):
207
+ super(HandlerMenu, self).__init__(title="Handlers", parent=parent)
208
+ self.logger = logger
209
+
210
+ def install_handler(self, name):
211
+ plugins.add_logging_handler(self.logger, name)
212
+
213
+ def refresh(self):
214
+ self.clear()
215
+ # Add the Install sub menu showing all logging_handler plugins
216
+ handler_install = self.addMenu('Install')
217
+ for name, cls in plugins.logging_handlers():
218
+ act = handler_install.addAction(name)
219
+ act.triggered.connect(partial(self.install_handler, name))
220
+ for h in self.logger.handlers:
221
+ if type(h) is cls:
222
+ act.setEnabled(False)
223
+ act.setToolTip('Already installed for this logger.')
224
+ break
225
+
226
+ # Add a visual indication of all of the existing handlers
227
+ # TODO: Add ability to modify the formatters and auto-creation on startup
228
+ self.addSeparator()
229
+ for handler in self.logger.handlers:
230
+ act = self.addAction(repr(handler))
231
+ act.setEnabled(False)
232
+
233
+
234
+ class LoggingLevelMenu(LazyMenu):
235
+
236
+ """
237
+ Custom menu for Python Loggers.
238
+
239
+ Provides an interface for changing logger levels via menu actions. Also
240
+ displays the presently set level by highlighting the relevant menu action
241
+ and via the menu's icon (which displays the logger's effective level,
242
+ potentially inherited from its ancestor).
243
+ """
244
+
245
+ def __init__(self, name, logger, parent=None):
246
+ """
247
+ Creates the default level menu actions for updating the logger's level.
248
+
249
+ Args:
250
+ name (str): Name of Logger this menu will represent.
251
+ logger (logging.Logger): Logger this menu will represent and control
252
+ via actions that modify the logger's set level.
253
+ parent (QToolButton/QMenu): `QMenu` or `QToolButton` this menu will
254
+ be parented to.
255
+
256
+ Note: If the logger is merely a placeholder it will be initialized so
257
+ it can be added to the menu hierarchy. This ensures all ancestors
258
+ exist for appropriate parenting when descendants are added.
259
+ """
260
+ super(LoggingLevelMenu, self).__init__(title=name.split(".")[-1], parent=parent)
261
+
262
+ if isinstance(logger, logging.PlaceHolder):
263
+ logger = logging.getLogger(name)
264
+
265
+ self.logger = logger
266
+ self.name = name
267
+ self.update_ui()
268
+
269
+ def children(self):
270
+ """The direct sub-loggers of this logging object."""
271
+ parent = self.name
272
+ if parent == "root":
273
+ parent = ""
274
+ for name, logger in sorted(
275
+ logging.root.manager.loggerDict.items(), key=lambda x: x[0].lower()
276
+ ):
277
+ if name.startswith(parent):
278
+ remaining = name.lstrip(parent).lstrip(".")
279
+ if remaining and "." not in remaining:
280
+ yield name, logger
281
+
282
+ def level(self):
283
+ """Returns the current effective LoggerLevel for self.logger."""
284
+ effective_level = self.logger.getEffectiveLevel()
285
+ effective_level_name = logging.getLevelName(effective_level)
286
+ return LoggerLevels.fromLabel(effective_level_name, logger=self.logger)
287
+
288
+ def refresh(self):
289
+ self.clear()
290
+
291
+ self.addMenu(HandlerMenu(self.logger, self))
292
+ self.addSeparator()
293
+
294
+ if self.logger.disabled:
295
+ # Warn the user that this logger has been disabled. The documentation
296
+ # says to consider this property as read only so this is just info
297
+ # and does not implement a way to re-enable this logger. It's not
298
+ # disabled because that would prevent the tooltip from showing.
299
+ action = self.addAction("Logger is disabled")
300
+ action.setToolTip(
301
+ 'This <b>logger has been disabled</b> and does not handle events '
302
+ "so you likely won't see events from it or its children. "
303
+ 'This normally happens when using logging.config with '
304
+ '"disable_existing_loggers" defaulted to True.'
305
+ )
306
+ action.setIcon(QIcon(resourcePath('img/warning-big.png')))
307
+ self.addSeparator()
308
+
309
+ for logger_level in LoggerLevels:
310
+ is_current = logger_level.is_current(self.logger)
311
+
312
+ action = QAction(logger_level.icon, logger_level.name, self)
313
+ action.setCheckable(True)
314
+ action.setChecked(is_current)
315
+
316
+ # tooltip example: "Set 'preditor.debug' to level Warning")
317
+ action.setToolTip(
318
+ "Set '{}' to level {}".format(self.name, logger_level.name)
319
+ )
320
+
321
+ # when clicked/activated set associated loggers level
322
+ action.triggered.connect(partial(self.setLevel, logger_level.number))
323
+ self.addAction(action)
324
+
325
+ self.addSeparator()
326
+
327
+ for name, child in self.children():
328
+ self.addMenu(LoggingLevelMenu(name, child, self))
329
+
330
+ def setLevel(self, level):
331
+ """
332
+ Sets the logger this menu object represents to the level supplied.
333
+
334
+ Args:
335
+ level (str): Logging level to set logger to.
336
+ """
337
+ self.logger.setLevel(level)
338
+ self.update_ui()
339
+
340
+ def update_ui(self):
341
+ """Set the menu icon to this LoggerLevel's icon.
342
+
343
+ If the updated logger is the root logger, the logging level toolbar
344
+ button's icon is updated instead.
345
+
346
+ Args:
347
+ level (LoggerLevel): Logging level to change icon to represent.
348
+ """
349
+
350
+ level_enum = self.level()
351
+ act = self.menuAction()
352
+ act.setIcon(level_enum.icon)
353
+ act.setToolTip(
354
+ "Logger '{}' current level: {}".format(self.logger.name, level_enum.name)
355
+ )
356
+
357
+ if self.name == "root":
358
+ self.parent().refresh()