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.
- preditor/__init__.py +315 -0
- preditor/__main__.py +13 -0
- preditor/about_module.py +165 -0
- preditor/cli.py +192 -0
- preditor/config.py +318 -0
- preditor/constants.py +13 -0
- preditor/contexts.py +210 -0
- preditor/cores/__init__.py +0 -0
- preditor/cores/core.py +20 -0
- preditor/dccs/.hab.json +10 -0
- preditor/dccs/maya/PrEditor_maya.mod +1 -0
- preditor/dccs/maya/README.md +22 -0
- preditor/dccs/maya/plug-ins/PrEditor_maya.py +141 -0
- preditor/dccs/studiomax/PackageContents.xml +32 -0
- preditor/dccs/studiomax/PrEditor-PrEditor_Show.mcr +8 -0
- preditor/dccs/studiomax/README.md +17 -0
- preditor/dccs/studiomax/preditor.ms +16 -0
- preditor/dccs/studiomax/preditor_menu.mnx +7 -0
- preditor/debug.py +149 -0
- preditor/delayable_engine/__init__.py +302 -0
- preditor/delayable_engine/delayables.py +85 -0
- preditor/enum.py +728 -0
- preditor/excepthooks.py +165 -0
- preditor/gui/__init__.py +56 -0
- preditor/gui/app.py +163 -0
- preditor/gui/codehighlighter.py +289 -0
- preditor/gui/completer.py +237 -0
- preditor/gui/console.py +605 -0
- preditor/gui/console_base.py +911 -0
- preditor/gui/dialog.py +181 -0
- preditor/gui/drag_tab_bar.py +625 -0
- preditor/gui/editor_chooser.py +57 -0
- preditor/gui/errordialog.py +69 -0
- preditor/gui/find_files.py +137 -0
- preditor/gui/fuzzy_search/__init__.py +0 -0
- preditor/gui/fuzzy_search/fuzzy_search.py +97 -0
- preditor/gui/group_tab_widget/__init__.py +0 -0
- preditor/gui/group_tab_widget/group_tab_widget.py +528 -0
- preditor/gui/group_tab_widget/grouped_tab_menu.py +35 -0
- preditor/gui/group_tab_widget/grouped_tab_models.py +107 -0
- preditor/gui/group_tab_widget/grouped_tab_widget.py +223 -0
- preditor/gui/group_tab_widget/one_tab_widget.py +96 -0
- preditor/gui/level_buttons.py +358 -0
- preditor/gui/logger_window_handler.py +77 -0
- preditor/gui/logger_window_plugin.py +35 -0
- preditor/gui/loggerwindow.py +2405 -0
- preditor/gui/newtabwidget.py +69 -0
- preditor/gui/output_console.py +11 -0
- preditor/gui/qtdesigner/__init__.py +21 -0
- preditor/gui/qtdesigner/_log_plugin.py +29 -0
- preditor/gui/qtdesigner/console_base_plugin.py +48 -0
- preditor/gui/qtdesigner/console_predit_plugin.py +48 -0
- preditor/gui/set_text_editor_path_dialog.py +61 -0
- preditor/gui/status_label.py +99 -0
- preditor/gui/suggest_path_quotes_dialog.py +50 -0
- preditor/gui/ui/editor_chooser.ui +93 -0
- preditor/gui/ui/errordialog.ui +74 -0
- preditor/gui/ui/find_files.ui +140 -0
- preditor/gui/ui/loggerwindow.ui +1909 -0
- preditor/gui/ui/set_text_editor_path_dialog.ui +189 -0
- preditor/gui/ui/suggest_path_quotes_dialog.ui +225 -0
- preditor/gui/window.py +161 -0
- preditor/gui/workbox_mixin.py +1139 -0
- preditor/gui/workbox_text_edit.py +136 -0
- preditor/gui/workboxwidget.py +315 -0
- preditor/logging_config.py +55 -0
- preditor/osystem.py +401 -0
- preditor/plugins.py +118 -0
- preditor/prefs.py +381 -0
- preditor/resource/environment_variables.html +26 -0
- preditor/resource/error_mail.html +85 -0
- preditor/resource/error_mail_inline.html +41 -0
- preditor/resource/img/README.md +17 -0
- preditor/resource/img/arrow_forward.png +0 -0
- preditor/resource/img/check-bold.png +0 -0
- preditor/resource/img/chevron-down.png +0 -0
- preditor/resource/img/chevron-up.png +0 -0
- preditor/resource/img/close-thick.png +0 -0
- preditor/resource/img/comment-edit.png +0 -0
- preditor/resource/img/content-copy.png +0 -0
- preditor/resource/img/content-cut.png +0 -0
- preditor/resource/img/content-duplicate.png +0 -0
- preditor/resource/img/content-paste.png +0 -0
- preditor/resource/img/content-save.png +0 -0
- preditor/resource/img/debug_disabled.png +0 -0
- preditor/resource/img/eye-check.png +0 -0
- preditor/resource/img/file-plus.png +0 -0
- preditor/resource/img/file-remove.png +0 -0
- preditor/resource/img/format-align-left.png +0 -0
- preditor/resource/img/format-letter-case-lower.png +0 -0
- preditor/resource/img/format-letter-case-upper.png +0 -0
- preditor/resource/img/format-letter-case.svg +1 -0
- preditor/resource/img/information.png +0 -0
- preditor/resource/img/logging_critical.png +0 -0
- preditor/resource/img/logging_custom.png +0 -0
- preditor/resource/img/logging_debug.png +0 -0
- preditor/resource/img/logging_error.png +0 -0
- preditor/resource/img/logging_info.png +0 -0
- preditor/resource/img/logging_not_set.png +0 -0
- preditor/resource/img/logging_warning.png +0 -0
- preditor/resource/img/marker.png +0 -0
- preditor/resource/img/play.png +0 -0
- preditor/resource/img/playlist-play.png +0 -0
- preditor/resource/img/plus-minus-variant.png +0 -0
- preditor/resource/img/preditor.ico +0 -0
- preditor/resource/img/preditor.png +0 -0
- preditor/resource/img/preditor.psd +0 -0
- preditor/resource/img/preditor.svg +44 -0
- preditor/resource/img/regex.svg +1 -0
- preditor/resource/img/restart.svg +1 -0
- preditor/resource/img/skip-forward-outline.png +0 -0
- preditor/resource/img/skip-next-outline.png +0 -0
- preditor/resource/img/skip-next.png +0 -0
- preditor/resource/img/skip-previous.png +0 -0
- preditor/resource/img/subdirectory-arrow-right.png +0 -0
- preditor/resource/img/text-search-variant.png +0 -0
- preditor/resource/img/warning-big.png +0 -0
- preditor/resource/lang/python.json +30 -0
- preditor/resource/pref_updates/pref_updates.json +17 -0
- preditor/resource/settings.ini +25 -0
- preditor/resource/stylesheet/Bright.css +76 -0
- preditor/resource/stylesheet/Dark.css +210 -0
- preditor/scintilla/__init__.py +40 -0
- preditor/scintilla/delayables/__init__.py +11 -0
- preditor/scintilla/delayables/smart_highlight.py +97 -0
- preditor/scintilla/delayables/spell_check.py +174 -0
- preditor/scintilla/documenteditor.py +1924 -0
- preditor/scintilla/finddialog.py +68 -0
- preditor/scintilla/lang/__init__.py +80 -0
- preditor/scintilla/lang/config/bash.ini +15 -0
- preditor/scintilla/lang/config/batch.ini +14 -0
- preditor/scintilla/lang/config/cpp.ini +19 -0
- preditor/scintilla/lang/config/css.ini +19 -0
- preditor/scintilla/lang/config/eyeonscript.ini +17 -0
- preditor/scintilla/lang/config/html.ini +21 -0
- preditor/scintilla/lang/config/javascript.ini +24 -0
- preditor/scintilla/lang/config/lua.ini +16 -0
- preditor/scintilla/lang/config/maxscript.ini +20 -0
- preditor/scintilla/lang/config/mel.ini +18 -0
- preditor/scintilla/lang/config/mu.ini +22 -0
- preditor/scintilla/lang/config/nsi.ini +19 -0
- preditor/scintilla/lang/config/perl.ini +19 -0
- preditor/scintilla/lang/config/puppet.ini +19 -0
- preditor/scintilla/lang/config/python.ini +28 -0
- preditor/scintilla/lang/config/ruby.ini +19 -0
- preditor/scintilla/lang/config/sql.ini +7 -0
- preditor/scintilla/lang/config/xml.ini +21 -0
- preditor/scintilla/lang/config/yaml.ini +18 -0
- preditor/scintilla/lang/language.py +240 -0
- preditor/scintilla/lexers/__init__.py +0 -0
- preditor/scintilla/lexers/cpplexer.py +22 -0
- preditor/scintilla/lexers/javascriptlexer.py +27 -0
- preditor/scintilla/lexers/maxscriptlexer.py +235 -0
- preditor/scintilla/lexers/mellexer.py +369 -0
- preditor/scintilla/lexers/mulexer.py +33 -0
- preditor/scintilla/lexers/pythonlexer.py +42 -0
- preditor/scintilla/ui/finddialog.ui +160 -0
- preditor/settings.py +71 -0
- preditor/stream/__init__.py +72 -0
- preditor/stream/console_handler.py +169 -0
- preditor/stream/director.py +144 -0
- preditor/stream/manager.py +97 -0
- preditor/streamhandler_helper.py +46 -0
- preditor/utils/__init__.py +191 -0
- preditor/utils/call_stack.py +86 -0
- preditor/utils/cute.py +106 -0
- preditor/utils/stylesheets.py +54 -0
- preditor/utils/text_search.py +338 -0
- preditor/version.py +34 -0
- preditor/weakref.py +363 -0
- preditor-2.1.0.dist-info/METADATA +308 -0
- preditor-2.1.0.dist-info/RECORD +179 -0
- preditor-2.1.0.dist-info/WHEEL +5 -0
- preditor-2.1.0.dist-info/entry_points.txt +19 -0
- preditor-2.1.0.dist-info/licenses/LICENSE +165 -0
- preditor-2.1.0.dist-info/top_level.txt +3 -0
- tests/encodings/test_ecoding.py +33 -0
- tests/find_files/test_find_files.py +74 -0
- 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,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)
|