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
preditor/settings.py ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python
2
+ from __future__ import absolute_import, print_function
3
+
4
+ import os
5
+ import sys
6
+
7
+ try:
8
+ import configparser
9
+ except Exception:
10
+ import ConfigParser as configparser # noqa: N813
11
+
12
+ # define the default environment variables
13
+ OS_TYPE = ''
14
+ if os.name == 'posix':
15
+ OS_TYPE = 'Linux'
16
+ elif os.name == 'nt':
17
+ OS_TYPE = 'Windows'
18
+ elif os.name == 'osx':
19
+ OS_TYPE = 'MacOS'
20
+
21
+ # The sections to add from settings.ini
22
+ # The order matters. Add from most specific to least specific.
23
+ # Example: Add Windows Offline, then Windows, then Default.
24
+ # Environment variables that exist in os.environ will not be added.
25
+ _SECTIONS_TO_ADD = []
26
+ if os.getenv('BDEV_OFFLINE') == '1':
27
+ _SECTIONS_TO_ADD.append('{} Offline'.format(OS_TYPE))
28
+ _SECTIONS_TO_ADD += [OS_TYPE, 'Default']
29
+
30
+ _currentEnv = ''
31
+ defaults = {}
32
+
33
+
34
+ def environStr(value):
35
+ if sys.version_info[0] > 2:
36
+ # Python 3 requires a unicode value. aka str(), which these values already are
37
+ return value
38
+ # Python 2 requires str object, not unicode
39
+ return value.encode('utf8')
40
+
41
+
42
+ def addConfigSection(config_parser, section):
43
+ """
44
+ Add a config section to os.environ for a section.
45
+
46
+ Does not add options that already exist in os.environ.
47
+
48
+ Args:
49
+ config_parser (configparser.RawConfigParser): The parser to read from.
50
+ Must already be read.
51
+ section (str): The section name to add.
52
+ """
53
+
54
+ for option in config_parser.options(section):
55
+ if option.upper() not in os.environ:
56
+ value = config_parser.get(section, option)
57
+ if value == 'None':
58
+ value = ''
59
+ # In python2.7 on windows you can't pass unicode values to
60
+ # subprocess.Popen's env argument. This is the reason we are calling str()
61
+ os.environ[environStr(option.upper())] = environStr(value)
62
+
63
+
64
+ # load the default environment from the settings INI
65
+ config = configparser.RawConfigParser()
66
+ config.read(os.path.join(os.path.dirname(__file__), 'resource', 'settings.ini'))
67
+ for section in _SECTIONS_TO_ADD:
68
+ addConfigSection(config, section)
69
+
70
+ # store the blurdev path in the environment
71
+ os.environ['BDEV_PATH'] = environStr(os.path.dirname(__file__))
@@ -0,0 +1,72 @@
1
+ """ A system for capturing stream output and later inserting the captured output into a
2
+ gui, and allowing that GUI to directly capture any new output written to the streams.
3
+
4
+ A use case for this is to attach a Manager's as early as possible to ``sys.stdout``
5
+ and ``sys.stderr`` process, before any GUI's are created. Then later you initialize a
6
+ GUI python console that should show all python output that has already been written.
7
+ Any future writes are delivered directly to the gui using a callback mechanism.
8
+
9
+ Example::
10
+
11
+ # Startup script or plugin for DCC runs this as early as possible.
12
+ from preditor.stream import install_to_std
13
+
14
+ manager = install_to_std()
15
+ # Startup script exits and DCC continues to load eventually creating the main gui.
16
+
17
+ # From a menu a user chooses to show a custom console(A gui that replicates a
18
+ # python interactive console inside the DCC).
19
+ console = PythonConsole()
20
+
21
+ # We will want to see all the previous writes made by python, so replay them
22
+ # in the console so it can properly handle stdout and stderr writes.
23
+ for msg, state in manager:
24
+ console.write(msg, state)
25
+
26
+ # Make it so any future writes are automatically added to the console.
27
+ manager.add_callback(console.write)
28
+
29
+ # Optionally, disable storing data in the buffer. buffer.write calls will now
30
+ # directly write to console so there is no reason to duplicate the data to the
31
+ # buffer.
32
+ manager.append_writes = False
33
+ """
34
+ import sys
35
+
36
+ from ..constants import StreamType
37
+ from ..streamhandler_helper import StreamHandlerHelper # noqa: E402
38
+ from .director import Director # noqa: E402
39
+ from .manager import Manager # noqa: E402
40
+
41
+ """Set when :py:attr:``install_to_std`` is called. This stores the installed Manager
42
+ so it can be accessed to install callbacks.
43
+ """
44
+ active = None
45
+
46
+ __all__ = ["active", "Director", "install_to_std", "Manager"]
47
+
48
+
49
+ def install_to_std(out=True, err=True):
50
+ """Replaces ``sys.stdout`` and ``sys.stderr`` with :py:class:`Director`'s
51
+ using the returned :py:class:`Manager`. This manager is stored as the ``active``
52
+ variable and can be accessed later. This can be called more than once, and it will
53
+ simply return the already installed Manager.
54
+
55
+ Args:
56
+ out (bool, optional): Enables replacement of ``sys.stdout`` on first call.
57
+ err (bool, optional): Enables replacement of ``sys.stderr`` on first call.
58
+ """
59
+ global active
60
+
61
+ if active is None:
62
+ active = Manager()
63
+ if out:
64
+ sys.stdout = Director(active, StreamType.STDOUT)
65
+ # Update any StreamHandler's that were setup using the old stdout
66
+ StreamHandlerHelper.replace_stream(sys.stdout.old_stream, sys.stdout)
67
+ if err:
68
+ sys.stderr = Director(active, StreamType.STDERR)
69
+ # Update any StreamHandler's that were setup using the old stderr
70
+ StreamHandlerHelper.replace_stream(sys.stderr.old_stream, sys.stderr)
71
+
72
+ return active
@@ -0,0 +1,169 @@
1
+ from __future__ import absolute_import
2
+
3
+ import logging
4
+ import re
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from .. import plugins
9
+ from ..constants import StreamType
10
+ from . import Manager
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class DefaultDescriptor:
16
+ def __init__(self, *, ident=None, default=None):
17
+ self._default = default
18
+ self._ident = ident
19
+
20
+ def __set_name__(self, owner, name):
21
+ self._name = "_" + name
22
+
23
+ def __get__(self, obj, type):
24
+ if obj is None:
25
+ return self._default
26
+
27
+ return getattr(obj, self._name, self._default)
28
+
29
+
30
+ class LoggingLevelDescriptor(DefaultDescriptor):
31
+ """Converts a string into a logging level or None.
32
+
33
+ When set, the string will be converted to an int if possible otherwise the
34
+ provided value is stored.
35
+ """
36
+
37
+ _level_conversion = logging.Handler()
38
+
39
+ def __set__(self, obj, value):
40
+ if value is None:
41
+ value = logging.NOTSET
42
+ else:
43
+ try:
44
+ value = int(value)
45
+ except ValueError:
46
+ # convert logging level strings to their int values if possible.
47
+ try:
48
+ self._level_conversion.setLevel(value)
49
+ value = self._level_conversion.level
50
+ except Exception:
51
+ logger.warning(f"Unable to convert {value} to an int logging level")
52
+ value = 0
53
+ setattr(obj, self._name, value)
54
+
55
+
56
+ class FormatterDescriptor(DefaultDescriptor):
57
+ """Stores a logging.Formatter object.
58
+
59
+ If a string is passed it will be cast into a Formatter instance.
60
+ """
61
+
62
+ def __init__(self, *, ident=None, default=None):
63
+ if isinstance(default, str):
64
+ default = logging.Formatter(default)
65
+ super().__init__(ident=ident, default=default)
66
+
67
+ def __set__(self, obj, value):
68
+ if isinstance(value, str):
69
+ value = logging.Formatter(value)
70
+ setattr(obj, self._name, value)
71
+
72
+
73
+ @dataclass
74
+ class HandlerInfo:
75
+ name: str
76
+ level: LoggingLevelDescriptor = LoggingLevelDescriptor()
77
+ plugin: Optional[str] = "Console"
78
+ formatter: FormatterDescriptor = FormatterDescriptor()
79
+
80
+ _attr_names = {"plug": "plugin", "fmt": "formatter", "lvl": "level"}
81
+
82
+ def __post_init__(self):
83
+ # Clear self.name so you can define omit name to define a root logger
84
+ # For example passing "level=INFO" would set the root logger to info.
85
+ name = self.name
86
+ self.name = ""
87
+
88
+ parts = self.__to_parts__(name)
89
+ for i, value in enumerate(parts):
90
+ key, _value = self.__parse_setting__(value, i)
91
+ setattr(self, key, _value)
92
+
93
+ @classmethod
94
+ def __to_parts__(cls, value):
95
+ """Returns a list of args and kwargs. Kwargs are 2 item tuples."""
96
+ sp = re.split(r'(\w+(?<!\\)=)', value)
97
+ args = [i for i in sp[0].split(',') if i]
98
+ for i in range(1, len(sp), 2):
99
+ value = sp[i + 1].replace("\\=", "=")
100
+ if value.endswith(","):
101
+ value = value[:-1]
102
+ args.append((sp[i][:-1], value))
103
+ return args
104
+
105
+ @classmethod
106
+ def __parse_setting__(cls, value, index):
107
+ """Converts a value into its name and value.
108
+
109
+ Examples:
110
+ ("preditor", 0) returns ("name", "preditor")
111
+ ("lvl=DEBUG", 0) returns ("level", "INFO")
112
+ """
113
+ if isinstance(value, tuple):
114
+ # The string specified the key so expand it to the property name.
115
+ attr_name = cls._attr_names.get(value[0], value[0])
116
+ value = value[1]
117
+ else:
118
+ # If there is no key defined, map the index to the property name
119
+ i = list(cls.__dataclass_fields__)[index]
120
+ field = cls.__dataclass_fields__[i]
121
+ attr_name = field.name
122
+
123
+ return attr_name, value
124
+
125
+ def install(self, callback=None, replay=False, disable_writes=False, clear=False):
126
+ """Add the required logging handler if needed and connect callback to it."""
127
+ _logger = logging.getLogger(self.name)
128
+ handler, _ = plugins.add_logging_handler(_logger, self.plugin)
129
+ if handler and callback:
130
+ handler.manager.add_callback(
131
+ callback, replay=replay, disable_writes=disable_writes, clear=clear
132
+ )
133
+ return handler
134
+
135
+ def uninstall(self, callback):
136
+ """Remove the callback added via install, doesn't remove the logging handler."""
137
+ _logger = logging.getLogger(self.name)
138
+ handler, _ = plugins.add_logging_handler(_logger, self.plugin)
139
+ if handler:
140
+ handler.manager.remove_callback(callback)
141
+
142
+
143
+ class ConsoleHandler(logging.Handler):
144
+ """A logging handler that writes directly to the PrEditor instance.
145
+
146
+ Args:
147
+ formatter (str or logging.Formatter, optional): If specified,
148
+ this is passed to setFormatter.
149
+ stream (optional): If provided write to this stream instead of the
150
+ main preditor instance's console.
151
+ stream_type (StreamType, optional): If not None, pass this value to the
152
+ write call's force kwarg.
153
+ """
154
+
155
+ def __init__(self):
156
+ super().__init__()
157
+ self.manager = Manager()
158
+
159
+ def emit(self, record):
160
+ try:
161
+ # If no gui has been created yet, or the `preditor.instance()` was
162
+ # closed and garbage collected, there is nothing to do, simply exit
163
+ # msg = self.format(record)
164
+ # self.manager.write(f'{msg}\n', StreamType.CONSOLE)
165
+ self.manager.write((self, record), StreamType.CONSOLE)
166
+ except (KeyboardInterrupt, SystemExit):
167
+ raise
168
+ except Exception:
169
+ self.handleError(record)
@@ -0,0 +1,144 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ import io
4
+ import sys
5
+
6
+ from ..constants import StreamType
7
+
8
+
9
+ class _DirectorBuffer(io.RawIOBase):
10
+ """Binary buffer that forwards text writes to the manager.
11
+
12
+ This makes the stream more compatible including if enabled when running tox.
13
+
14
+ Args:
15
+ manager (Manager): The manager that writes are stored in.
16
+ state: The state passed to the manager. This is often``StreamType.STDOUT``
17
+ or ``StreamType.STDERR``.
18
+ old_stream: A second stream that will be written to every time this stream
19
+ is written to. This allows this object to replace sys.stdout and still
20
+ send that output to the original stdout, which is useful for not breaking
21
+ DCC's script editors. Pass False to disable this feature. If you pass None
22
+ and state is set to ``StreamType.STDOUT`` or ``StreamType.STDERR``
23
+ this will automatically be set to the current sys.stdout or sys.stderr.
24
+ name (str, optional): Stored on self.name.
25
+ """
26
+
27
+ def __init__(self, manager, state, old_stream=None, name='nul'):
28
+ super().__init__()
29
+ self.manager = manager
30
+ self.state = state
31
+ self.old_stream = old_stream
32
+ self.name = name
33
+
34
+ def flush(self):
35
+ if self.old_stream:
36
+ self.old_stream.flush()
37
+ super().flush()
38
+
39
+ def writable(self):
40
+ return True
41
+
42
+ def write(self, b):
43
+ if isinstance(b, memoryview):
44
+ b = b.tobytes()
45
+
46
+ # Decode incoming bytes (TextIOWrapper encodes before sending here)
47
+ msg = b.decode("utf-8", errors="replace")
48
+ self.manager.write(msg, self.state)
49
+
50
+ if self.old_stream:
51
+ self.old_stream.write(msg)
52
+
53
+ return len(b)
54
+
55
+
56
+ class Director(io.TextIOWrapper):
57
+ """A file like object that stores the text written to it in a manager.
58
+ This manager can be shared between multiple Directors to build a single
59
+ continuous history of all writes.
60
+
61
+ While this uses a buffer under the hood, buffering is disabled and any calls
62
+ to write will automatically flush the buffer.
63
+
64
+ Args:
65
+ manager (Manager): The manager that writes are stored in.
66
+ state: The state passed to the manager. This is often ``StreamType.STDOUT``
67
+ or ``StreamType.STDERR``.
68
+ old_stream: A second stream that will be written to every time this stream
69
+ is written to. This allows this object to replace sys.stdout and still
70
+ send that output to the original stdout, which is useful for not breaking
71
+ DCC's script editors. Pass False to disable this feature. If you pass None
72
+ and state is set to ``StreamType.STDOUT`` or ``StreamType.STDERR``
73
+ this will automatically be set to the current sys.stdout or sys.stderr.
74
+ """
75
+
76
+ def __init__(self, manager, state, old_stream=None, *args, **kwargs):
77
+ # Keep track of whether we wrapped a std stream
78
+ # that way we don't .close() any streams that we don't control
79
+ self.std_stream_wrapped = False
80
+
81
+ name = 'nul'
82
+ if old_stream is False:
83
+ old_stream = None
84
+ elif old_stream is None:
85
+ if state == StreamType.STDOUT:
86
+ # On Windows if we're in pythonw.exe, then sys.stdout is named "nul"
87
+ # And it uses cp1252 encoding (which breaks with unicode)
88
+ # So if we find this nul TextIOWrapper, it's safe to just skip it
89
+ name = getattr(sys.stdout, 'name', '')
90
+ if name != 'nul':
91
+ self.std_stream_wrapped = True
92
+ old_stream = sys.stdout
93
+ elif state == StreamType.STDERR:
94
+ name = getattr(sys.stderr, 'name', '')
95
+ if name != 'nul':
96
+ self.std_stream_wrapped = True
97
+ old_stream = sys.stderr
98
+
99
+ self.old_stream = old_stream
100
+ self.manager = manager
101
+ self.state = state
102
+
103
+ # Build the buffer. This provides the expected interface for tox, etc.
104
+ raw = _DirectorBuffer(manager, state, old_stream, name)
105
+ buffer = io.BufferedWriter(raw)
106
+
107
+ super().__init__(buffer, encoding="utf-8", write_through=True, *args, **kwargs)
108
+
109
+ def __repr__(self):
110
+ return f"<Director state={self.state} old_stream={self.old_stream!r}>"
111
+
112
+ def close(self):
113
+ if (
114
+ self.old_stream
115
+ and not self.std_stream_wrapped
116
+ and self.old_stream is not sys.__stdout__
117
+ and self.old_stream is not sys.__stderr__
118
+ ):
119
+ self.old_stream.close()
120
+
121
+ super().close()
122
+
123
+ def write(self, msg):
124
+ super().write(msg)
125
+ # Force a write of any buffered data
126
+ self.flush()
127
+
128
+ # These methods enable terminal features like color coding etc.
129
+ def isatty(self):
130
+ if self.old_stream is not None:
131
+ return self.old_stream.isatty()
132
+ return False
133
+
134
+ @property
135
+ def encoding(self):
136
+ if self.old_stream is not None:
137
+ return self.old_stream.encoding
138
+ return super().encoding
139
+
140
+ @property
141
+ def errors(self):
142
+ if self.old_stream is not None:
143
+ return self.old_stream.errors
144
+ return super().errors
@@ -0,0 +1,97 @@
1
+ from __future__ import absolute_import, print_function
2
+
3
+ import collections
4
+
5
+ from .. import utils
6
+ from ..weakref import WeakList
7
+
8
+
9
+ class Manager(collections.deque):
10
+ """Stores all of the data from the stdout/stderr writes. You can iterate over this
11
+ object to see all of the (msg, state) calls that have been written to it up to the
12
+ maxlen specified when constructing it.
13
+
14
+ Args:
15
+ maxlen (int, optional): The maximum number of raw writes to store. If this is
16
+ exceeded, the oldest writes are discarded.
17
+
18
+ Properties:
19
+ store_writes (bool): Set this to False if you no longer want write calls to
20
+ store on the manager.
21
+ """
22
+
23
+ def __init__(self, maxlen=10000):
24
+ super(Manager, self).__init__(maxlen=maxlen)
25
+ self.callbacks = WeakList()
26
+ self.store_writes = True
27
+
28
+ def add_callback(self, callback, replay=False, disable_writes=False, clear=False):
29
+ """Add a callable that will be called every time write is called.
30
+
31
+ Args:
32
+ callback (callable): A callable object that takes two arguments. It must
33
+ take two arguments (msg, state). See write for more details.
34
+ replay (bool, optional): If True, calls replay on callback.
35
+ disable_writes (bool, optional): Set store_writes to False if this is True.
36
+ clear (bool, optional): Clear the stored history on this object.
37
+
38
+ Returns:
39
+ bool: True if the callback was added. If the callback has already been
40
+ already, this method does nothing and returns False.
41
+ """
42
+ if callback in self.callbacks:
43
+ return False
44
+
45
+ self.callbacks.append(callback)
46
+
47
+ if replay:
48
+ self.replay(callback)
49
+
50
+ if disable_writes:
51
+ # Disable storing data in the buffer. buffer.write calls will now
52
+ # directly write to console so there is no reason to duplicate the
53
+ # data to the buffer.
54
+ self.store_writes = False
55
+
56
+ if clear:
57
+ self.clear()
58
+
59
+ return True
60
+
61
+ def remove_callback(self, callback) -> bool:
62
+ """Remove callback from manager and return if it was removed."""
63
+ if callback not in self.callbacks:
64
+ return False
65
+ self.callbacks.remove(callback)
66
+ return True
67
+
68
+ def replay(self, callback):
69
+ """Replay the existing writes for the given callback.
70
+
71
+ This iterates over all the stored writes and pass them to callback. This
72
+ is useful for when you are initializing a gui and want to include all
73
+ previous prints.
74
+ """
75
+ for msg, state in self:
76
+ callback(msg, state)
77
+
78
+ def get_value(self, fmt="[{state}:{msg}]"):
79
+ return ''.join([fmt.format(msg=d[0], state=d[1]) for d in self])
80
+
81
+ def write(self, msg, state):
82
+ """Adds the written text to the manager and passes it to any attached callbacks.
83
+
84
+ Args:
85
+ msg (str): The text to be written.
86
+ state: A identifier for how the text is to be written. For example if this
87
+ write is coming from sys.stderr this will likely be set to
88
+ ``preditor.constants.StreamType.STDERR``.
89
+ """
90
+ if self.store_writes:
91
+ self.append((msg, state))
92
+
93
+ for callback in self.callbacks:
94
+ try:
95
+ callback(msg, state)
96
+ except Exception:
97
+ utils.ShellPrint(True).print_exc("PrEditor Console failed")
@@ -0,0 +1,46 @@
1
+ from __future__ import absolute_import
2
+
3
+ import logging
4
+ import sys
5
+
6
+
7
+ class StreamHandlerHelper(object):
8
+ """A collection of functions for manipulating ``logging.StreamHandler`` objects."""
9
+
10
+ @classmethod
11
+ def set_stream(cls, handler, stream):
12
+ """For the given StreamHandler, set its stream. This works around
13
+ python 2's lack of StreamHandler.setStream by replicating python 3.
14
+ """
15
+ # TODO: once python 2 is no longer supported, replace any uses of this
16
+ # function with `handler.setStream(stream)`
17
+ if sys.version_info[0] > 2:
18
+ handler.setStream(stream)
19
+ else:
20
+ # Copied from python 3's logging's setStream to work in python 2
21
+ handler.acquire()
22
+ try:
23
+ handler.flush()
24
+ handler.stream = stream
25
+ finally:
26
+ handler.release()
27
+
28
+ @classmethod
29
+ def replace_stream(cls, old, new, logger=None):
30
+ """Replaces the stream of StreamHandlers by checking all
31
+ `logging.StreamHandler`'s attached to the provided logger. If any of them are
32
+ using old for their stream, update that stream to new.
33
+
34
+ Args:
35
+ old (stream): Only StreamHandlers using this stream will be updated to new.
36
+ new (stream): A file stream object like `sys.stderr` that will replace old.
37
+ logger (logging.Logger, optional): The logger to update streams for. If
38
+ None, the root logger(`logging.getLogger()`) will be used.
39
+ """
40
+ if logger is None:
41
+ logger = logging.getLogger()
42
+
43
+ for handler in logger.handlers:
44
+ if isinstance(handler, logging.StreamHandler):
45
+ if handler.stream == old:
46
+ cls.set_stream(handler, new)