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,141 @@
1
+ from __future__ import absolute_import
2
+
3
+ import os
4
+ import site
5
+ from pathlib import Path
6
+
7
+ import maya.mel
8
+ from maya import OpenMayaUI, cmds
9
+
10
+ preditor_menu = None
11
+
12
+
13
+ def headless():
14
+ """If true, no Qt gui elements should be used because python is running a
15
+ QCoreApplication."""
16
+ return bool(cmds.about(batch=True))
17
+
18
+ # TODO: This is the old method for detecting batch mode. Remove this once
19
+ # the above about command is vetted as working.
20
+ # basename = os.path.splitext(os.path.basename(sys.executable).lower())[0]
21
+ # return basename in ('mayabatch', 'mayapy')
22
+
23
+
24
+ def root_window():
25
+ """Returns the main window of Maya as a Qt object to be used for parenting."""
26
+ from Qt import QtCompat
27
+
28
+ ptr = OpenMayaUI.MQtUtil.mainWindow()
29
+ if ptr is not None:
30
+ pointer = int(ptr)
31
+ return QtCompat.wrapInstance(pointer)
32
+
33
+
34
+ def launch(ignored):
35
+ """Show the PrEditor GUI and bring it into focus if it was minimized."""
36
+ import preditor
37
+
38
+ widget = preditor.launch()
39
+ return widget
40
+
41
+
42
+ def update_site():
43
+ """Adds a site dir to python. This makes its contents importable to python.
44
+
45
+ This includes making any editable installs located in that site-packages folder
46
+ accessible to this python instance. This does not activate the virtualenv.
47
+
48
+ If the env var `PREDITOR_SITE` is set, this path is used. Otherwise the
49
+ parent directory of preditor is used.
50
+
51
+ - `PREDITOR_SITE` is useful if you want to use an editable install of preditor
52
+ for development. This should point to a virtualenv's site-packages folder.
53
+ - Otherwise if the virtualenv has a regular pip install of preditor you can
54
+ skip setting the env var.
55
+ """
56
+ venv_path = os.getenv("PREDITOR_SITE")
57
+ # If the env var is not defined then use the parent dir of this preditor package.
58
+ if venv_path is None:
59
+ venv_path = cmds.moduleInfo(moduleName="PrEditor", path=True)
60
+ venv_path = Path(venv_path).parent.parent.parent
61
+ venv_path = str(venv_path)
62
+
63
+ print(f"Preditor is adding python site: {venv_path}")
64
+ site.addsitedir(venv_path)
65
+
66
+
67
+ def initializePlugin(mobject): # noqa: N802
68
+ """Initialize the script plug-in"""
69
+ global preditor_menu
70
+
71
+ # If running headless, there is no need to build a gui and create the python logger
72
+ if not headless():
73
+ update_site()
74
+
75
+ from Qt.QtWidgets import QApplication
76
+
77
+ import preditor
78
+
79
+ maya_ver = cmds.about(version=True).split(" ")[0]
80
+
81
+ # Capture all stderr/out after the plugin is loaded. This makes it so
82
+ # if the PrEditor GUI is shown, it will include all of the output. Also
83
+ # tells PrEditor how to parent itself to the main window and save prefs.
84
+ preditor.configure(
85
+ # Set the core_name so preferences are saved per-maya version.
86
+ "Maya-{}".format(maya_ver),
87
+ # Tell PrEditor how to find the maya root window for parenting
88
+ parent_callback=root_window,
89
+ # Tell it how to check if running in batch mode
90
+ headless_callback=headless,
91
+ )
92
+
93
+ # Detect Maya shutting down and ensure PrEditor's prefs are saved
94
+ if QApplication.instance():
95
+ QApplication.instance().aboutToQuit.connect(preditor.shutdown)
96
+
97
+ # Add a new PrEditor menu with an item that launches PrEditor
98
+ gmainwindow = maya.mel.eval("$temp1=$gMainWindow")
99
+ preditor_menu = cmds.menu(label="PrEditor", parent=gmainwindow, tearOff=True)
100
+ cmds.menuItem(
101
+ label="PrEditor",
102
+ command=launch,
103
+ sourceType="python",
104
+ image=preditor.resourcePath('img/preditor.png'),
105
+ parent=preditor_menu,
106
+ )
107
+
108
+ # TODO: Alternatively figure out how to add the launcher menuItem to a
109
+ # pre-existing maya menu like next to the "Script Editor" in
110
+ # "Windows -> General Editors"
111
+ # https://github.com/chadmv/cvwrap/blob/master/scripts/cvwrap/menu.py#L18
112
+
113
+ # menu = 'mainWindowMenu'
114
+ # # Make sure the menu widgets exist first.
115
+ # maya.mel.eval('ChaDeformationsMenu MayaWindow|{0};'.format(menu))
116
+ # items = cmds.menu(menu, q=True, ia=True)
117
+ # # print(items)
118
+ # for item in items:
119
+ # menu_label = cmds.menuItem(item, q=True, label=True)
120
+ # # print(menu_label)
121
+ # if menu_label == "General Editors":
122
+ # # cmds.menuItem(parent=item, divider=True, dividerLabel='PrEditor' )
123
+ # cmds.menuItem(
124
+ # label="PrEditor",
125
+ # command=launch,
126
+ # sourceType='python',
127
+ # image=preditor.resourcePath('img/preditor.png'),
128
+ # parent=item,
129
+ # )
130
+
131
+
132
+ def uninitializePlugin(mobject): # noqa: N802
133
+ """Uninitialize the script plug-in"""
134
+ import preditor
135
+
136
+ # Remove the PrEditor Menu if it exists
137
+ if preditor_menu and cmds.menu(preditor_menu, exists=True):
138
+ cmds.deleteUI(preditor_menu, menu=True)
139
+
140
+ # Close PrEditor making sure to save prefs
141
+ preditor.core.shutdown()
@@ -0,0 +1,32 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <ApplicationPackage
3
+ SchemaVersion="1.0"
4
+ AutodeskProduct="3ds Max"
5
+ ProductType="Application"
6
+ Name="PrEditor"
7
+ Description="Initialize PrEditor inside 3ds Max."
8
+ AppVersion="0.0.1"
9
+ ProductCode="{7b1e4ece-9a30-4afb-99ed-b9fb7c23e2f9}"
10
+ UpgradeCode="{2d245bb4-19cf-4f90-ae19-3ff006ed0f48}">
11
+ <CompanyDetails Name="Blur Studio" Email="opensource@blur.com"/>
12
+ <Components Description="pre-start-up scripts parts">
13
+ <RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
14
+ <ComponentEntry ModuleName="./preditor.ms" />
15
+ </Components>
16
+ <Components Description="macroscripts parts">
17
+ <RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
18
+ <ComponentEntry ModuleName="./PrEditor-PrEditor_Show.mcr" />
19
+ </Components>
20
+ <Components Description="menu parts">
21
+ <RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
22
+ <ComponentEntry ModuleName="./preditor_menu.mnx" />
23
+ </Components>
24
+ <Components Description="light icon paths parts">
25
+ <RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
26
+ <ComponentEntry AppName="lighticons" Version="1.0.0" ModuleName="../../resource/img" />
27
+ </Components>
28
+ <Components Description="dark icon paths parts">
29
+ <RuntimeRequirements OS="Win64" Platform="3ds Max" SeriesMin="2025" SeriesMax="2026" />
30
+ <ComponentEntry AppName="darkicons" Version="1.0.0" ModuleName="../../resource/img" />
31
+ </Components>
32
+ </ApplicationPackage>
@@ -0,0 +1,8 @@
1
+ macroScript PrEditor_Show
2
+ category:"PrEditor"
3
+ tooltip:"PrEditor..."
4
+ IconName:"preditor.ico"
5
+ (
6
+ local preditor = python.import "preditor"
7
+ preditor.launch()
8
+ )
@@ -0,0 +1,17 @@
1
+ # 3ds Max Integration
2
+
3
+ This is an example of using an Autodesk Application Package to load PrEditor into
4
+ 3ds Max. This adds a PrEditor item to the Scripting menu in 3ds Max's menu bar
5
+ to show PrEditor. It adds the excepthook so if a python exception is raised
6
+ it will prompt the user to show PrEditor. PrEditor will show all python stdout/stderr
7
+ output generated after the plugin is loaded.
8
+
9
+ # Setup
10
+
11
+ Make sure to follow these [setup instructions](/preditor/README.md#Setup) first to create the virtualenv.
12
+
13
+ # Use
14
+
15
+ The [preditor/dccs/studiomax](/preditor/dccs/studiomax) directory is setup as a 3ds Max Application Plugin.
16
+ To load it in 3ds Max add the full path to that directory to the `ADSK_APPLICATION_PLUGINS` environment
17
+ variable. You can use `;` on windows and `:` on linux to join multiple paths together.
@@ -0,0 +1,16 @@
1
+
2
+ function configure_preditor = (
3
+ local pysite = python.import "site"
4
+ local WinEnv = dotNetClass "System.Environment"
5
+ -- If the env var PREDITOR_SITE is set, add its packages to python
6
+ local venv_path = WinEnv.GetEnvironmentVariable "PREDITOR_SITE"
7
+ if venv_path != undefined do (
8
+ print("Preditor is adding python site: " + venv_path)
9
+ pysite.addsitedir venv_path
10
+ )
11
+ -- Configure preditor, adding excepthook etc.
12
+ local preditor = python.import "preditor"
13
+ preditor.configure "studiomax"
14
+ )
15
+
16
+ configure_preditor()
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <MaxMenuTransformations>
3
+ <CreateMenuSeparator MenuId="658724ec-de09-47dd-b723-918c59a28ad1" Id="9cdc28de-9e70-471c-ae5c-d86e3b7fefba" BeforeId="4e206a37-f502-4fa8-ba0e-70dd427d507f"/>
4
+ <MoveItem DestinationId="658724ec-de09-47dd-b723-918c59a28ad1" ItemId="4e206a37-f502-4fa8-ba0e-70dd427d507f"/>
5
+ <CreateMenuAction MenuId="658724ec-de09-47dd-b723-918c59a28ad1" Id="f243229b-f0f6-42b4-a94d-bebe9ef9777f" ActionId="647394-PrEditor_Show`PrEditor"/>
6
+ <MoveItem DestinationId="658724ec-de09-47dd-b723-918c59a28ad1" ItemId="9cdc28de-9e70-471c-ae5c-d86e3b7fefba" BeforeId="f243229b-f0f6-42b4-a94d-bebe9ef9777f"/>
7
+ </MaxMenuTransformations>
preditor/debug.py ADDED
@@ -0,0 +1,149 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ import datetime
4
+ import inspect
5
+ import logging
6
+ import sys
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class FileLogger:
12
+ def __init__(self, stdhandle, logfile, _print=True, clearLog=True):
13
+ self.old_stream = stdhandle
14
+ self._logfile = logfile
15
+ self._print = _print
16
+ if clearLog:
17
+ # clear the log file
18
+ self.clear()
19
+
20
+ def clear(self, stamp=False):
21
+ """Removes the contents of the log file."""
22
+ open(self._logfile, 'w', newline="\n", encoding="utf-8").close()
23
+ if stamp:
24
+ print(self.stamp())
25
+
26
+ def flush(self):
27
+ if self.old_stream:
28
+ self.old_stream.flush()
29
+
30
+ def stamp(self):
31
+ msg = '--------- Date: {today} Version: {version} ---------'
32
+ return msg.format(today=datetime.datetime.today(), version=sys.version)
33
+
34
+ def write(self, msg):
35
+ # Newline forces windows to write unix style newlines
36
+ with open(self._logfile, 'a', newline="\n", encoding="utf-8") as f:
37
+ f.write(msg)
38
+
39
+ if self._print:
40
+ self.old_stream.write(msg)
41
+
42
+
43
+ def logToFile(path, stdout=True, stderr=True, useOldStd=True, clearLog=True):
44
+ """Redirect all stdout and/or stderr output to a log file.
45
+
46
+ Creates a FileLogger class for stdout and stderr and installs itself in python.
47
+ All output will be logged to the file path. Prints the current datetime and
48
+ sys.version info when stdout is True.
49
+
50
+ Args:
51
+ path (str): File path to log output to.
52
+
53
+ stdout (bool): If True(default) override sys.stdout.
54
+
55
+ stderr (bool): If True(default) override sys.stderr.
56
+
57
+ useOldStd (bool): If True, messages will be written to the FileLogger
58
+ and the previous sys.stdout/sys.stderr.
59
+
60
+ clearLog (bool): If True(default) clear the log file when this command is
61
+ called.
62
+ """
63
+ if stderr:
64
+ sys.stderr = FileLogger(sys.stderr, path, useOldStd, clearLog=clearLog)
65
+ if stdout:
66
+ sys.stdout = FileLogger(sys.stdout, path, useOldStd, clearLog=False)
67
+ if clearLog:
68
+ sys.stdout.clear(stamp=True)
69
+
70
+ from .streamhandler_helper import StreamHandlerHelper
71
+
72
+ # Update any StreamHandler's that were setup using the old stdout/err
73
+ if stdout:
74
+ StreamHandlerHelper.replace_stream(sys.stdout.old_stream, sys.stdout)
75
+ if stderr:
76
+ StreamHandlerHelper.replace_stream(sys.stderr.old_stream, sys.stderr)
77
+
78
+
79
+ def printCallingFunction(compact=False):
80
+ """Prints and returns info about the calling function
81
+
82
+ Args:
83
+ compact (bool): If set to True, prints a more compact printout
84
+
85
+ Returns:
86
+ str: Info on the calling function.
87
+ """
88
+ import inspect
89
+
90
+ current = inspect.currentframe().f_back
91
+ try:
92
+ parent = current.f_back
93
+ except AttributeError:
94
+ print('No Calling function found')
95
+ return
96
+ currentInfo = inspect.getframeinfo(current)
97
+ parentInfo = inspect.getframeinfo(parent)
98
+ if parentInfo[3] is not None:
99
+ context = ', '.join(parentInfo[3]).strip('\t').rstrip()
100
+ else:
101
+ context = 'No context to return'
102
+ if compact:
103
+ output = '# %s Calling Function: %s Filename: %s Line: %i Context: %s' % (
104
+ currentInfo[2],
105
+ parentInfo[2],
106
+ parentInfo[0],
107
+ parentInfo[1],
108
+ context,
109
+ )
110
+ else:
111
+ output = ["Function: '%s' in file '%s'" % (currentInfo[2], currentInfo[0])]
112
+ output.append(
113
+ " Calling Function: '%s' in file '%s'" % (parentInfo[2], parentInfo[0])
114
+ )
115
+ output.append(" Line: '%i'" % parentInfo[1])
116
+ output.append(" Context: '%s'" % context)
117
+ output = '\n'.join(output)
118
+ print(output)
119
+ return output
120
+
121
+
122
+ def mroDump(obj, nice=True, joinString='\n'):
123
+ """Formats inspect.getmro into text.
124
+
125
+ For the given class object or instance of a class, use inspect to return the Method
126
+ Resolution Order.
127
+
128
+ Args: obj (object): The object to return the mro of. This can be a class object or
129
+ instance.1
130
+
131
+ nice (bool): Returns the same module names as help(object) if True, otherwise
132
+ repr(object).
133
+
134
+ joinString (str, optional): The repr of each class is joined by this string.
135
+
136
+ Returns:
137
+ str: A string showing the Method Resolution Order of the given object.
138
+ """
139
+ import pydoc
140
+
141
+ # getmro requires a class, turn instances into a class
142
+ if not inspect.isclass(obj):
143
+ obj = type(obj)
144
+ classes = inspect.getmro(obj)
145
+ if nice:
146
+ ret = [pydoc.classname(x, obj.__module__) for x in (classes)]
147
+ else:
148
+ ret = [repr(x) for x in (classes)]
149
+ return joinString.join(ret)
@@ -0,0 +1,302 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ import time
4
+ import warnings
5
+ import weakref
6
+ from collections import OrderedDict
7
+ from collections.abc import MutableSet
8
+
9
+ from Qt import QtCompat
10
+ from Qt.QtCore import QObject, QTimer, Signal
11
+
12
+ from .delayables import Delayable
13
+
14
+
15
+ # https://stackoverflow.com/a/7829569
16
+ class OrderedSet(MutableSet):
17
+ def __init__(self, values=()):
18
+ self._od = OrderedDict().fromkeys(values)
19
+
20
+ def __len__(self):
21
+ return len(self._od)
22
+
23
+ def __iter__(self):
24
+ return iter(self._od)
25
+
26
+ def __contains__(self, value):
27
+ return value in self._od
28
+
29
+ def add(self, value):
30
+ self._od[value] = None
31
+
32
+ def discard(self, value):
33
+ self._od.pop(value, None)
34
+
35
+
36
+ class OrderedWeakrefSet(weakref.WeakSet):
37
+ def __init__(self, values=()):
38
+ super(OrderedWeakrefSet, self).__init__()
39
+ self.data = OrderedSet()
40
+ for elem in values:
41
+ self.add(elem)
42
+
43
+
44
+ class DelayableEngine(QObject):
45
+ """Provides a way for multiple DocumentEditors to run code over
46
+ multiple Qt event loops in chunks preventing locking up the ui.
47
+
48
+ Signals:
49
+ processing_finished (int, float): Emitted when the engine finishes
50
+ processing successfully.
51
+ """
52
+
53
+ _instance = {}
54
+ processing_finished = Signal()
55
+
56
+ def __init__(self, name, parent=None, interval=0):
57
+ super(DelayableEngine, self).__init__()
58
+ self.name = name
59
+ self.documents = OrderedWeakrefSet()
60
+ self.delayables = {}
61
+ self.maxLoopTime = 0.01
62
+ self.start_time = time.time()
63
+ # It's likely we wont' finish processing all documents and all delayables
64
+ # before we run out of time. These variables keep track of where we stopped.
65
+ self.document_index = 0
66
+ self.delayable_index = 0
67
+
68
+ self.timer = QTimer(self)
69
+ self.timer.setInterval(interval)
70
+ self.timer.timeout.connect(self.loop)
71
+
72
+ # These values are reset when enqueue needs to start self.timer
73
+ # Each of these lists have a item added when self.loop exits
74
+ # (this can be it finished or ran out of time)
75
+ # Number of documents that had a delayable.loop called on them this loop
76
+ self.processed = []
77
+ # Time spent processing delayables for this self.loop
78
+ self.processing_time = []
79
+ # Number of nonVisible items that were skipped for this self.loop
80
+ self.skipped = []
81
+
82
+ def __repr__(self):
83
+ return '{}.{}("{}")'.format(
84
+ self.__module__,
85
+ self.__class__.__name__,
86
+ self.name,
87
+ )
88
+
89
+ def __str__(self):
90
+ return '{}("{}")'.format(self.__class__.__name__, self.name)
91
+
92
+ def add_delayable(self, delayable):
93
+ """Add a Delayable subclass instance for processing in this engine.
94
+
95
+ Args:
96
+ delayable (Delayable or str): A Delayable instance or the key identifier.
97
+ If a Delayable instance is passed, it will replace any previous
98
+ instances. If a string is passed it will not replace previous instance.
99
+
100
+ Raises:
101
+ KeyError: A invalid key identifier string was passed.
102
+ """
103
+ if isinstance(delayable, str):
104
+ if delayable in self.delayables:
105
+ # Don't replace the instance if a string is passed
106
+ return
107
+ for cls in Delayable._all_subclasses():
108
+ if cls.key == delayable:
109
+ delayable = cls(self)
110
+ break
111
+ else:
112
+ raise KeyError('No Delayable found with key: "{}"'.format(delayable))
113
+ elif delayable.key in self.delayables:
114
+ # Remove the old delayable if it exists so we can replace it.
115
+ self.remove_delayable(self.delayables[delayable.key])
116
+
117
+ self.delayables[delayable.key] = delayable
118
+ for document in self.documents:
119
+ delayable.add_document(document)
120
+
121
+ def add_document(self, document):
122
+ self.documents.add(document)
123
+ document.delayable_engine = self
124
+ for delayable in self.delayables:
125
+ self.delayables[delayable].add_document(document)
126
+
127
+ def add_supported_delayables(self, name):
128
+ """Add all valid Delayable subclasses that have name in their supports."""
129
+ for delayable in Delayable._all_subclasses():
130
+ if delayable.key not in self.delayables:
131
+ if name in delayable.supports and delayable.key != 'invalid':
132
+ self.add_delayable(delayable(self))
133
+
134
+ def delayable_enabled(self, delayable):
135
+ """Returns True if delayable is currently added.
136
+
137
+ Args:
138
+ delayable (Delayable or str): A Delayable instance or the key identifier.
139
+
140
+ Returns:
141
+ bool: Is the given delayable installed for this engine.
142
+ """
143
+ if isinstance(delayable, Delayable):
144
+ delayable = delayable.key
145
+ return delayable in self.delayables
146
+
147
+ def enqueue(self, document, key, *args):
148
+ # Only add a item to be processed if we have a delayable that can
149
+ # process the requested key.
150
+ if key in self.delayables:
151
+ # There is only ever one instance of a delayable class processed
152
+ # If we already have a class enqueued, merge the arguments so we
153
+ # don't end up loosing some processing.
154
+ if key in document.delayable_info:
155
+ args = self.delayables[key].merge_args(
156
+ document.delayable_info[key], args
157
+ )
158
+ document.delayable_info[key] = args
159
+ if not self.timer.isActive():
160
+ self.timer.start()
161
+ self.processed = []
162
+ self.processing_time = []
163
+ self.skipped = []
164
+ return True
165
+ return False
166
+
167
+ def expired(self):
168
+ return time.time() - self.start_time > self.maxLoopTime
169
+
170
+ @classmethod
171
+ def instance(cls, name, parent=None, interval=0):
172
+ """Returns a shared instance of DelayableEngine, creating the instance
173
+ if needed.
174
+
175
+ Args:
176
+ name (str): The name of the delayable engine to get the instance of.
177
+ parent (QWidget, optional): If a new instance is created, use this
178
+ as its parent. Ignored otherwise.
179
+ interval (int, optional): If a new instance is created, use this as
180
+ its interval value. Defaults to zero.
181
+ """
182
+ if name not in cls._instance:
183
+ cls._instance[name] = cls(name, parent=parent, interval=interval)
184
+ return cls._instance[name]
185
+
186
+ def loop(self): # noqa C901
187
+ self.start_time = time.time()
188
+ documents = list(self.documents)
189
+ # offset documents by the document_index so we can pickup where we left off
190
+ documents = documents[self.document_index :] + documents[: self.document_index]
191
+
192
+ count = 0
193
+ skipped = 0
194
+ finished = True
195
+ first_loop = True
196
+ while not self.expired():
197
+ for document in documents:
198
+ self.document_index += 1
199
+ if self.document_index >= len(documents):
200
+ self.document_index = 0
201
+
202
+ if not QtCompat.isValid(document):
203
+ if document in self.documents:
204
+ self.documents.remove(document)
205
+ print('Removing deleted document')
206
+ continue
207
+
208
+ if not document.delayable_info:
209
+ continue
210
+
211
+ if not document.isVisible() and first_loop:
212
+ skipped += 1
213
+ continue
214
+
215
+ keys = list(document.delayable_info.keys())
216
+ keys = keys[self.delayable_index :] + keys[: self.delayable_index]
217
+ for key in keys:
218
+ self.delayable_index += 1
219
+ if self.delayable_index > len(keys):
220
+ self.delayable_index = 0
221
+
222
+ # delayable_info should only have keys for delayables we can access.
223
+ delayable = self.delayables[key]
224
+
225
+ args = document.delayable_info[key]
226
+ try:
227
+ args = delayable.loop(document, *args)
228
+ except Exception:
229
+ warnings.warn('Error processing {}, canceling it'.format(key))
230
+ del document.delayable_info[key]
231
+ raise
232
+ if args:
233
+ document.delayable_info[key] = args
234
+ # We need to process more items
235
+ finished = False
236
+ else:
237
+ del document.delayable_info[key]
238
+ count += 1
239
+ if self.expired():
240
+ self.processed.append(count)
241
+ self.processing_time.append(time.time() - self.start_time)
242
+ self.skipped.append(skipped)
243
+ return
244
+
245
+ first_loop = False
246
+
247
+ self.processed.append(count)
248
+ self.processing_time.append(time.time() - self.start_time)
249
+ self.skipped.append(skipped)
250
+ if finished:
251
+ # Nothing else to do for now, just exit
252
+ self.timer.stop()
253
+ self.processing_finished.emit()
254
+
255
+ def remove_document(self, document):
256
+ """Removes a document from being processed"""
257
+ if document in self.documents:
258
+ for delayable in self.delayables:
259
+ self.delayables[delayable].remove_document(document)
260
+
261
+ self.documents.remove(document)
262
+ document.delayable_engine = type(self).instance('default')
263
+
264
+ def remove_delayable(self, delayable):
265
+ """Removes a Delayable subclass instance for processing in this engine.
266
+
267
+ Args:
268
+ delayable (Delayable or str): A Delayable instance or the key identifier.
269
+ Remove this delayable from the current documents if it was added.
270
+ """
271
+ if isinstance(delayable, str):
272
+ if delayable not in self.delayables:
273
+ return
274
+ delayable = self.delayables[delayable]
275
+ if delayable:
276
+ for document in self.documents:
277
+ delayable.remove_document(document)
278
+ # Stop processing this delayable if it's currently processing.
279
+ try:
280
+ del document.delayable_info[delayable.key]
281
+ except KeyError:
282
+ pass
283
+ self.delayables.pop(delayable.key)
284
+
285
+ def set_delayable_enabled(self, delayable, enabled):
286
+ """Add or remove the delayable provided.
287
+
288
+ Args:
289
+ delayable (Delayable or str): A Delayable instance or the key identifier.
290
+ enabled (bool): If True installs delayable, if False removes delayable.
291
+
292
+ See Also:
293
+ :py:meth:`DelayableEngine.add_delayable` and
294
+ :py:meth:`DelayableEngine.remove_delayable`
295
+
296
+ Raises:
297
+ KeyError: A invalid key identifier string was passed.
298
+ """
299
+ if enabled:
300
+ self.add_delayable(delayable)
301
+ else:
302
+ self.remove_delayable(delayable)