pyqttoolkit 0.9.1__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.
- pyqttoolkit/__init__.py +20 -0
- pyqttoolkit/_version.py +21 -0
- pyqttoolkit/application/__init__.py +18 -0
- pyqttoolkit/application/application.py +86 -0
- pyqttoolkit/application/garbage_collector.py +65 -0
- pyqttoolkit/colors/__init__.py +84 -0
- pyqttoolkit/config/__init__.py +20 -0
- pyqttoolkit/config/base.py +22 -0
- pyqttoolkit/config/composite.py +31 -0
- pyqttoolkit/config/default.py +27 -0
- pyqttoolkit/config/environment.py +35 -0
- pyqttoolkit/config/multi_json_file.py +93 -0
- pyqttoolkit/config/with_signal.py +30 -0
- pyqttoolkit/config/with_value_changed.py +31 -0
- pyqttoolkit/data/__init__.py +21 -0
- pyqttoolkit/data/bits.py +23 -0
- pyqttoolkit/data/cache.py +64 -0
- pyqttoolkit/data/interval.py +26 -0
- pyqttoolkit/data/message.py +29 -0
- pyqttoolkit/data/names.py +24 -0
- pyqttoolkit/datetime/__init__.py +16 -0
- pyqttoolkit/datetime/datetime.py +51 -0
- pyqttoolkit/datetime/qdatetime.py +53 -0
- pyqttoolkit/dependencies/__init__.py +56 -0
- pyqttoolkit/errors/__init__.py +19 -0
- pyqttoolkit/exceptions/__init__.py +18 -0
- pyqttoolkit/filesystem/__init__.py +24 -0
- pyqttoolkit/logs/__init__.py +140 -0
- pyqttoolkit/metadata/__init__.py +23 -0
- pyqttoolkit/metadata/dataframe_metadata_manager.py +105 -0
- pyqttoolkit/metadata/dict_metadata_manager.py +63 -0
- pyqttoolkit/metadata/id_generator.py +20 -0
- pyqttoolkit/metadata/metadata_manager_base.py +54 -0
- pyqttoolkit/models/__init__.py +26 -0
- pyqttoolkit/models/code_text.py +43 -0
- pyqttoolkit/models/dataframetable.py +135 -0
- pyqttoolkit/models/dictpropertytree.py +155 -0
- pyqttoolkit/models/dicttable.py +140 -0
- pyqttoolkit/models/json_text.py +52 -0
- pyqttoolkit/models/list_model.py +52 -0
- pyqttoolkit/models/module.py +26 -0
- pyqttoolkit/models/roles.py +23 -0
- pyqttoolkit/models/series_data.py +68 -0
- pyqttoolkit/models/span.py +36 -0
- pyqttoolkit/models/validation.py +27 -0
- pyqttoolkit/modules/__init__.py +18 -0
- pyqttoolkit/modules/events/__init__.py +19 -0
- pyqttoolkit/modules/events/add_signal.py +31 -0
- pyqttoolkit/modules/events/module_opening.py +21 -0
- pyqttoolkit/modules/events/rejectable.py +29 -0
- pyqttoolkit/modules/launcher_config.py +55 -0
- pyqttoolkit/modules/module.py +111 -0
- pyqttoolkit/properties/__init__.py +18 -0
- pyqttoolkit/properties/auto.py +221 -0
- pyqttoolkit/properties/throttled.py +77 -0
- pyqttoolkit/scripting/__init__.py +19 -0
- pyqttoolkit/scripting/script_context.py +36 -0
- pyqttoolkit/scripting/script_runner.py +132 -0
- pyqttoolkit/scripting/script_template.py +42 -0
- pyqttoolkit/services/__init__.py +32 -0
- pyqttoolkit/services/autosave.py +41 -0
- pyqttoolkit/services/email.py +25 -0
- pyqttoolkit/services/event_registry.py +52 -0
- pyqttoolkit/services/file_dialog.py +71 -0
- pyqttoolkit/services/hotkey_manager.py +137 -0
- pyqttoolkit/services/link_manager.py +135 -0
- pyqttoolkit/services/message_board.py +90 -0
- pyqttoolkit/services/module_service.py +154 -0
- pyqttoolkit/services/object_converter.py +45 -0
- pyqttoolkit/services/project_updater.py +149 -0
- pyqttoolkit/services/task_runner.py +281 -0
- pyqttoolkit/services/theme_manager.py +46 -0
- pyqttoolkit/services/tool_window_service.py +46 -0
- pyqttoolkit/viewmanagers/__init__.py +17 -0
- pyqttoolkit/viewmanagers/tool.py +23 -0
- pyqttoolkit/views/__init__.py +50 -0
- pyqttoolkit/views/bulk_value_selector.py +112 -0
- pyqttoolkit/views/check_box.py +36 -0
- pyqttoolkit/views/code/__init__.py +18 -0
- pyqttoolkit/views/code/code_edit.py +268 -0
- pyqttoolkit/views/code/python_syntax.py +226 -0
- pyqttoolkit/views/combo_box.py +101 -0
- pyqttoolkit/views/count_selector.py +59 -0
- pyqttoolkit/views/datetime.py +111 -0
- pyqttoolkit/views/datetime_range_selector.py +231 -0
- pyqttoolkit/views/default_text_line_edit.py +50 -0
- pyqttoolkit/views/delegates.py +117 -0
- pyqttoolkit/views/dtype_edit.py +117 -0
- pyqttoolkit/views/editable_combo_box.py +45 -0
- pyqttoolkit/views/file_selector.py +51 -0
- pyqttoolkit/views/icon.py +67 -0
- pyqttoolkit/views/icon_button.py +129 -0
- pyqttoolkit/views/json_edit.py +21 -0
- pyqttoolkit/views/labelled.py +29 -0
- pyqttoolkit/views/layout/__init__.py +21 -0
- pyqttoolkit/views/layout/collabsible.py +59 -0
- pyqttoolkit/views/layout/group_box.py +48 -0
- pyqttoolkit/views/layout/group_panel.py +188 -0
- pyqttoolkit/views/layout/lines.py +30 -0
- pyqttoolkit/views/layout/link.py +38 -0
- pyqttoolkit/views/line_edit.py +61 -0
- pyqttoolkit/views/linkable.py +49 -0
- pyqttoolkit/views/list_view.py +43 -0
- pyqttoolkit/views/main_window.py +91 -0
- pyqttoolkit/views/metadata_selector.py +84 -0
- pyqttoolkit/views/modulewindow.py +81 -0
- pyqttoolkit/views/plot/__init__.py +26 -0
- pyqttoolkit/views/plot/legend_control.py +121 -0
- pyqttoolkit/views/plot/matplotlib/__init__.py +23 -0
- pyqttoolkit/views/plot/matplotlib/base.py +1056 -0
- pyqttoolkit/views/plot/matplotlib/colorbar.py +75 -0
- pyqttoolkit/views/plot/matplotlib/colormap.py +48 -0
- pyqttoolkit/views/plot/matplotlib/font.py +55 -0
- pyqttoolkit/views/plot/plot_options.py +211 -0
- pyqttoolkit/views/plot/plot_toolbar_options.py +133 -0
- pyqttoolkit/views/plot/size.py +41 -0
- pyqttoolkit/views/plot/tool_type.py +26 -0
- pyqttoolkit/views/plot/toolbar.py +100 -0
- pyqttoolkit/views/popout.py +41 -0
- pyqttoolkit/views/popup.py +41 -0
- pyqttoolkit/views/property_tree.py +26 -0
- pyqttoolkit/views/shrunk_push_button.py +34 -0
- pyqttoolkit/views/steppable_combo_box.py +67 -0
- pyqttoolkit/views/styleable.py +37 -0
- pyqttoolkit/views/table.py +146 -0
- pyqttoolkit/views/text_edit.py +31 -0
- pyqttoolkit/views/tool_window.py +35 -0
- pyqttoolkit-0.9.1.dist-info/METADATA +69 -0
- pyqttoolkit-0.9.1.dist-info/RECORD +133 -0
- pyqttoolkit-0.9.1.dist-info/WHEEL +5 -0
- pyqttoolkit-0.9.1.dist-info/licenses/LICENSE.txt +674 -0
- pyqttoolkit-0.9.1.dist-info/licenses/LICENSE_SHORT +16 -0
- pyqttoolkit-0.9.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
""":mod:`toolkit`
|
|
18
|
+
Defines various utils useful in views
|
|
19
|
+
"""
|
|
20
|
+
import logging
|
|
21
|
+
from math import isnan
|
|
22
|
+
from weakref import ref, WeakKeyDictionary
|
|
23
|
+
|
|
24
|
+
from pyqttoolkit.logs import TRACE
|
|
25
|
+
|
|
26
|
+
LOGGER = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
def updater(obj, name):
|
|
29
|
+
"""function::updater(obj, name)
|
|
30
|
+
Higher order function which returns a function to update the property
|
|
31
|
+
specified by name in the object specified by obj
|
|
32
|
+
"""
|
|
33
|
+
def _update_value(value):
|
|
34
|
+
setattr(obj, name, value)
|
|
35
|
+
return _update_value
|
|
36
|
+
|
|
37
|
+
def one_way_bind(src, dest, src_name, dest_name=None):
|
|
38
|
+
"""function::one_way_bind(src, dest, src_name, [dest_name])
|
|
39
|
+
Propagate changes in src to dest, and set dest to value of src
|
|
40
|
+
"""
|
|
41
|
+
dest_name = dest_name or src_name
|
|
42
|
+
src_signal = getattr(type(src), AutoProperty.get_notify_property_name(src_name))
|
|
43
|
+
dest_prop = getattr(type(dest), dest_name)
|
|
44
|
+
src_signal.__get__(src, type(src)).connect(dest_prop.updater(dest))
|
|
45
|
+
src_value = getattr(src, src_name)
|
|
46
|
+
setattr(dest, dest_name, src_value)
|
|
47
|
+
return updater
|
|
48
|
+
|
|
49
|
+
def bind(src, dest, src_name, dest_name=None, two_way=True):
|
|
50
|
+
"""function::bind(src, dest, src_name, [dest_name[, two_way]])
|
|
51
|
+
Propagate changes in src to dest, and set dest to value of src
|
|
52
|
+
Optionally bind dest to src
|
|
53
|
+
"""
|
|
54
|
+
dest_name = dest_name or src_name
|
|
55
|
+
one_way_bind(src, dest, src_name, dest_name)
|
|
56
|
+
if two_way:
|
|
57
|
+
one_way_bind(dest, src, dest_name, src_name)
|
|
58
|
+
|
|
59
|
+
def one_way_unbind(src, dest, src_name, dest_name=None):
|
|
60
|
+
dest_name = dest_name or src_name
|
|
61
|
+
src_signal = getattr(type(src), AutoProperty.get_notify_property_name(src_name))
|
|
62
|
+
dest_prop = getattr(type(dest), dest_name)
|
|
63
|
+
src_signal.__get__(src, type(src)).disconnect(dest_prop.updater(dest))
|
|
64
|
+
|
|
65
|
+
def unbind(src, dest, src_name, dest_name=None, two_way=True):
|
|
66
|
+
dest_name = dest_name or src_name
|
|
67
|
+
one_way_unbind(src, dest, src_name, dest_name)
|
|
68
|
+
if two_way:
|
|
69
|
+
one_way_unbind(dest, src, dest_name, src_name)
|
|
70
|
+
|
|
71
|
+
def auto_property(type_, *args, **kwargs):
|
|
72
|
+
"""function::auto_property(type, *args, **kwargs)
|
|
73
|
+
Decorator function for AutoProperty, creates an AutoProperty with the given getter
|
|
74
|
+
"""
|
|
75
|
+
def _(fget):
|
|
76
|
+
return AutoProperty(type_=type_, fget=fget, *args, **kwargs)
|
|
77
|
+
return _
|
|
78
|
+
|
|
79
|
+
def values_equal(left, right):
|
|
80
|
+
return left == right or (isinstance(left, float) and isinstance(right, float) and isnan(left) and isnan(right))
|
|
81
|
+
|
|
82
|
+
def connect_all(handler, *signals):
|
|
83
|
+
for signal in signals:
|
|
84
|
+
signal.connect(handler)
|
|
85
|
+
|
|
86
|
+
def disconnect_all(handler, *signals):
|
|
87
|
+
for signal in signals:
|
|
88
|
+
signal.disconnect(handler)
|
|
89
|
+
|
|
90
|
+
#pylint: disable=too-many-arguments
|
|
91
|
+
class AutoProperty:
|
|
92
|
+
"""class::AutoProperty
|
|
93
|
+
Descriptor which provides property and signal functionality to objects
|
|
94
|
+
"""
|
|
95
|
+
native_types = [list, str, int, float, bool]
|
|
96
|
+
def __init__(self, type_, fget=None, fset=None, fdel=None, default_value=None, notify=None):
|
|
97
|
+
self._type = type_
|
|
98
|
+
self._default_value = default_value
|
|
99
|
+
self._notify = notify
|
|
100
|
+
self._name = None
|
|
101
|
+
self._fget = fget
|
|
102
|
+
self._fset = fset
|
|
103
|
+
self._fdel = fdel
|
|
104
|
+
self._updaters = WeakKeyDictionary()
|
|
105
|
+
self._notify_depth = 0
|
|
106
|
+
self._max_notify_depth = 5
|
|
107
|
+
|
|
108
|
+
def __set_name__(self, obj, name):
|
|
109
|
+
self._name = name
|
|
110
|
+
if not self._notify and self._has_notify_property(obj):
|
|
111
|
+
self._notify = self._get_notify_property(obj)
|
|
112
|
+
|
|
113
|
+
def __get__(self, obj, type_=None):
|
|
114
|
+
if obj is None:
|
|
115
|
+
return self
|
|
116
|
+
if not self._name:
|
|
117
|
+
raise ValueError('Cannot get property, name has not been set!')
|
|
118
|
+
if self._uses_delegates():
|
|
119
|
+
if not self._fget:
|
|
120
|
+
raise ValueError(f'No getter provided for property {self._name}')
|
|
121
|
+
return self._fget(obj)
|
|
122
|
+
elif self._name not in obj.__dict__:
|
|
123
|
+
return self._default_value
|
|
124
|
+
else:
|
|
125
|
+
return obj.__dict__[self._name]
|
|
126
|
+
|
|
127
|
+
def __set__(self, obj, value):
|
|
128
|
+
if obj is None:
|
|
129
|
+
return
|
|
130
|
+
if not self._name:
|
|
131
|
+
raise ValueError('Cannot set property, name has not been set!')
|
|
132
|
+
if not self._uses_delegates() and not self._notify:
|
|
133
|
+
raise ValueError(f'Cannot set property {self._name}, unable to notify!')
|
|
134
|
+
if value is not None and self._type and not isinstance(value, self._type):
|
|
135
|
+
value_type = type(value)
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f'Invalid type {value_type} provided for property {self._name}: '
|
|
138
|
+
f'must be of type {self._type}'
|
|
139
|
+
)
|
|
140
|
+
if self._uses_delegates():
|
|
141
|
+
if not self._fset:
|
|
142
|
+
raise ValueError(f'No setter provided for property {self._name}')
|
|
143
|
+
self._fset(obj, value)
|
|
144
|
+
elif self._name not in obj.__dict__ or not values_equal(obj.__dict__[self._name], value):
|
|
145
|
+
LOGGER.log(TRACE, 'Setting %s.%s to %s', obj, self._name, value)
|
|
146
|
+
obj.__dict__[self._name] = value
|
|
147
|
+
if self._notify_depth == self._max_notify_depth:
|
|
148
|
+
LOGGER.error('Maximum notify depth exceeded for %s.%s', obj, self._name)
|
|
149
|
+
if self._notify and self._notify_depth <= self._max_notify_depth:
|
|
150
|
+
self._notify_depth += 1
|
|
151
|
+
LOGGER.log(TRACE, 'Notifying change of %s.%s to %s', obj, self._name, value)
|
|
152
|
+
self._notify.__get__(obj, type(obj)).emit(value)
|
|
153
|
+
self._notify_depth -= 1
|
|
154
|
+
|
|
155
|
+
def __delete__(self, obj):
|
|
156
|
+
if not self._name:
|
|
157
|
+
raise ValueError('Cannot delete property, name has not been set!')
|
|
158
|
+
if self._uses_delegates():
|
|
159
|
+
if not self._fdel:
|
|
160
|
+
raise ValueError(f'No deleter provided for property {self._name}')
|
|
161
|
+
self._fdel(obj)
|
|
162
|
+
else:
|
|
163
|
+
del obj.__dict__[self._name]
|
|
164
|
+
|
|
165
|
+
def updater(self, obj, outer_value=None):
|
|
166
|
+
"""function::updater(self, obj, outer_value=None)
|
|
167
|
+
Returns a function that can be used to update with the specified outer_value
|
|
168
|
+
If outer_value is not specified, updates with the inner_value passed to the inner function
|
|
169
|
+
"""
|
|
170
|
+
if not obj in self._updaters:
|
|
171
|
+
obj_ref = ref(obj)
|
|
172
|
+
def _(inner_value=None):
|
|
173
|
+
self.__set__(obj_ref(), outer_value or inner_value)
|
|
174
|
+
self._updaters[obj] = _
|
|
175
|
+
return self._updaters[obj]
|
|
176
|
+
|
|
177
|
+
def getter(self, fget):
|
|
178
|
+
"""function::getter(self, fget)
|
|
179
|
+
Function decorator to create a getter
|
|
180
|
+
Returns a copy of this AutoProperty with fget as the getter
|
|
181
|
+
"""
|
|
182
|
+
return type(self)(
|
|
183
|
+
self._type, fget, self._fset, self._fdel,
|
|
184
|
+
self._default_value, self._notify
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def setter(self, fset):
|
|
188
|
+
"""function::setter(self, fset)
|
|
189
|
+
Function decorator to create a setter
|
|
190
|
+
Returns a copy of this AutoProperty with fset as the setter
|
|
191
|
+
"""
|
|
192
|
+
return type(self)(
|
|
193
|
+
self._type, self._fget, fset, self._fdel,
|
|
194
|
+
self._default_value, self._notify
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def deleter(self, fdel):
|
|
198
|
+
"""function::deleter(self, fdel)
|
|
199
|
+
Function decorator to create a deleter
|
|
200
|
+
Returns a copy of this AutoProperty with fdel as the deleter
|
|
201
|
+
"""
|
|
202
|
+
return type(self)(
|
|
203
|
+
self._type, self._fget, self._fset, fdel,
|
|
204
|
+
self._default_value, self._notify
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _get_notify_property(self, obj):
|
|
208
|
+
return getattr(obj, self.get_notify_property_name(self._name))
|
|
209
|
+
|
|
210
|
+
def _has_notify_property(self, obj):
|
|
211
|
+
return hasattr(obj, self.get_notify_property_name(self._name))
|
|
212
|
+
|
|
213
|
+
def _uses_delegates(self):
|
|
214
|
+
return self._fget or self._fset or self._fdel
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def get_notify_property_name(name):
|
|
218
|
+
"""function::get_notify_property_name(name)
|
|
219
|
+
Returns the name of the notify property
|
|
220
|
+
"""
|
|
221
|
+
return f'{name}Changed'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
import time
|
|
18
|
+
from inspect import signature
|
|
19
|
+
|
|
20
|
+
#pylint: disable=no-name-in-module
|
|
21
|
+
from PyQt5.QtCore import QTimer
|
|
22
|
+
#pylint: enable=no-name-in-module
|
|
23
|
+
|
|
24
|
+
def arg_count(f):
|
|
25
|
+
return len(signature(f).parameters)
|
|
26
|
+
|
|
27
|
+
class ThrottledBoundSignal:
|
|
28
|
+
def __init__(self, *types, interval=100):
|
|
29
|
+
self._types = types
|
|
30
|
+
self._interval = interval
|
|
31
|
+
self._functions = []
|
|
32
|
+
self._timer = None
|
|
33
|
+
self._last_event_time = 0
|
|
34
|
+
self._current_values = None
|
|
35
|
+
|
|
36
|
+
def emit(self, *values):
|
|
37
|
+
self._current_values = values
|
|
38
|
+
time_since_last_event = time.clock() - self._last_event_time
|
|
39
|
+
if self._timer is None and time_since_last_event >= (self._interval / 1000):
|
|
40
|
+
self._emit_now()
|
|
41
|
+
return
|
|
42
|
+
if self._timer is None:
|
|
43
|
+
self._timer = QTimer()
|
|
44
|
+
self._timer.setSingleShot(True)
|
|
45
|
+
self._timer.timeout.connect(self._emit_now)
|
|
46
|
+
self._timer.start(self._interval)
|
|
47
|
+
|
|
48
|
+
def _emit_now(self):
|
|
49
|
+
for f in self._functions:
|
|
50
|
+
f(*self._current_values[:arg_count(f)])
|
|
51
|
+
self._last_event_time = time.clock()
|
|
52
|
+
self._current_values = None
|
|
53
|
+
self._timer = None
|
|
54
|
+
|
|
55
|
+
def connect(self, f):
|
|
56
|
+
self._functions.append(f)
|
|
57
|
+
|
|
58
|
+
def disconnect(self, f):
|
|
59
|
+
self._functions.remove(f)
|
|
60
|
+
|
|
61
|
+
class ThrottledSignal:
|
|
62
|
+
def __init__(self, *types, interval=1000):
|
|
63
|
+
self._name = None
|
|
64
|
+
self._types = types
|
|
65
|
+
self._interval = interval
|
|
66
|
+
|
|
67
|
+
def __set_name__(self, obj, name):
|
|
68
|
+
self._name = name
|
|
69
|
+
|
|
70
|
+
def __get__(self, obj, type_=None):
|
|
71
|
+
if obj is None:
|
|
72
|
+
return self
|
|
73
|
+
if not self._name:
|
|
74
|
+
raise ValueError('Cannot get property, name has not been set!')
|
|
75
|
+
if self._name not in obj.__dict__:
|
|
76
|
+
obj.__dict__[self._name] = ThrottledBoundSignal(*self._types, interval=self._interval)
|
|
77
|
+
return obj.__dict__[self._name]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
from .script_runner import ScriptRunner
|
|
18
|
+
from .script_context import ScriptContext
|
|
19
|
+
from .script_template import ScriptTemplate
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
class ScriptContext:
|
|
18
|
+
def __init__(self, **kwargs):
|
|
19
|
+
self._locals = kwargs
|
|
20
|
+
self._result = {'value': None}
|
|
21
|
+
self._output = ''
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def locals(self):
|
|
25
|
+
return {**self._locals, 'result': self._result}
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def result(self):
|
|
29
|
+
return self._result['value']
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def output(self):
|
|
33
|
+
return self._output
|
|
34
|
+
|
|
35
|
+
def add_output(self, value):
|
|
36
|
+
self._output += value
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from functools import partial
|
|
19
|
+
import re
|
|
20
|
+
import builtins
|
|
21
|
+
import sys
|
|
22
|
+
import importlib
|
|
23
|
+
|
|
24
|
+
@contextmanager
|
|
25
|
+
def add_paths(paths):
|
|
26
|
+
for path in paths:
|
|
27
|
+
sys.path.insert(0, path)
|
|
28
|
+
try:
|
|
29
|
+
yield
|
|
30
|
+
finally:
|
|
31
|
+
for path in paths:
|
|
32
|
+
sys.path.remove(path)
|
|
33
|
+
|
|
34
|
+
class ContextWriter:
|
|
35
|
+
def __init__(self, context, skip_empty_output_lines=False):
|
|
36
|
+
self._context = context
|
|
37
|
+
self._skip_empty_output_lines = skip_empty_output_lines
|
|
38
|
+
|
|
39
|
+
def write(self, value):
|
|
40
|
+
if (value is not None and value.strip()) or not self._skip_empty_output_lines:
|
|
41
|
+
self._context.add_output(value)
|
|
42
|
+
|
|
43
|
+
@contextmanager
|
|
44
|
+
def redirect_output(context, skip_empty_output_lines=False):
|
|
45
|
+
writer = ContextWriter(context, skip_empty_output_lines)
|
|
46
|
+
_stdout = sys.stdout
|
|
47
|
+
sys.stdout = writer
|
|
48
|
+
try:
|
|
49
|
+
yield
|
|
50
|
+
finally:
|
|
51
|
+
sys.stdout = _stdout
|
|
52
|
+
|
|
53
|
+
class ScriptStopped(Exception):
|
|
54
|
+
def __init__(self):
|
|
55
|
+
Exception.__init__(self)
|
|
56
|
+
|
|
57
|
+
class ScriptRunner:
|
|
58
|
+
def __init__(self, additional_paths=None, skip_empty_output_lines=False):
|
|
59
|
+
self._disallowed_modules = [
|
|
60
|
+
r'\binspect\b', r'\bimp\b', r'\bsys\b', r'\b\.', r'\bbuiltins\b'
|
|
61
|
+
]
|
|
62
|
+
self._disallowed_globals = [
|
|
63
|
+
'_', 'quit', 'eval', 'exec', 'help'
|
|
64
|
+
]
|
|
65
|
+
self._additional_paths = additional_paths or []
|
|
66
|
+
self._context = None
|
|
67
|
+
self._skip_empty_output_lines = skip_empty_output_lines
|
|
68
|
+
|
|
69
|
+
def run(self, script, context):
|
|
70
|
+
script = re.sub(r'(?<!\w)result(?!\w)', 'result[\'value\']', script)
|
|
71
|
+
code = self.compile(script)
|
|
72
|
+
try:
|
|
73
|
+
self._context = context
|
|
74
|
+
with redirect_output(self._context, self._skip_empty_output_lines):
|
|
75
|
+
# Use one namespace so functions defined by the script can
|
|
76
|
+
# resolve each other recursively through their globals. Add
|
|
77
|
+
# runner globals last so context locals cannot replace
|
|
78
|
+
# protected entries such as __builtins__.
|
|
79
|
+
namespace = {**context.locals, **self._globals(context)}
|
|
80
|
+
#pylint: disable=exec-used
|
|
81
|
+
exec(code, namespace, namespace)
|
|
82
|
+
#pylint: enable=exec-used
|
|
83
|
+
except ScriptStopped:
|
|
84
|
+
pass
|
|
85
|
+
finally:
|
|
86
|
+
self._context = None
|
|
87
|
+
|
|
88
|
+
def _globals(self, context):
|
|
89
|
+
return {'__builtins__': {**__builtins__, '__import__': self._import(builtins.__import__), 'exit': self._exit, **{g: self._not_implemented for g in self._disallowed_globals}}}
|
|
90
|
+
|
|
91
|
+
def compile(self, script):
|
|
92
|
+
return compile(script, 'code', 'exec')
|
|
93
|
+
|
|
94
|
+
def _patch_plt_show(self, module):
|
|
95
|
+
def plt_show_as_show_figure(plt, block=None):
|
|
96
|
+
# Block is not used in the context of the script runner
|
|
97
|
+
for no, label in zip(plt.get_fignums(), plt.get_figlabels()):
|
|
98
|
+
self._context.show_figure(plt.figure(no), str(label))
|
|
99
|
+
plt.close('all')
|
|
100
|
+
if module.__name__ == 'matplotlib.pyplot':
|
|
101
|
+
module.show = partial(plt_show_as_show_figure, module)
|
|
102
|
+
if module.__name__ == 'matplotlib':
|
|
103
|
+
module.pyplot.show = partial(plt_show_as_show_figure, module.pyplot)
|
|
104
|
+
return module
|
|
105
|
+
|
|
106
|
+
def _import(self, import_function):
|
|
107
|
+
def _(module_name, *args, **kwargs):
|
|
108
|
+
if any(re.match(m, module_name) for m in self._disallowed_modules):
|
|
109
|
+
raise ImportError('Module not found')
|
|
110
|
+
try:
|
|
111
|
+
with add_paths(self._additional_paths):
|
|
112
|
+
module = import_function(module_name, *args, **kwargs)
|
|
113
|
+
if module_name in 'importlib':
|
|
114
|
+
module.import_module = self._import(importlib.import_module)
|
|
115
|
+
return self._patch_plt_show(module)
|
|
116
|
+
except KeyError:
|
|
117
|
+
raise ImportError('Module not found')
|
|
118
|
+
return _
|
|
119
|
+
|
|
120
|
+
def _additional_paths(self):
|
|
121
|
+
return self._additional_paths
|
|
122
|
+
|
|
123
|
+
def _exit(self):
|
|
124
|
+
raise ScriptStopped()
|
|
125
|
+
|
|
126
|
+
def add_path(self, path):
|
|
127
|
+
if path in self._additional_paths:
|
|
128
|
+
self._additional_paths.remove(path)
|
|
129
|
+
self._additional_paths.insert(0, path)
|
|
130
|
+
|
|
131
|
+
def _not_implemented(self, *args, **kwargs):
|
|
132
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
from jinja2 import Environment
|
|
20
|
+
|
|
21
|
+
class ScriptTemplate:
|
|
22
|
+
def __init__(self, template_string, name):
|
|
23
|
+
self._template_string = template_string
|
|
24
|
+
self._template_name = name
|
|
25
|
+
|
|
26
|
+
def render(self, *args, **kwargs):
|
|
27
|
+
environment = Environment()
|
|
28
|
+
environment.globals.update(**self._get_globals())
|
|
29
|
+
template = environment.from_string(self._template_string)
|
|
30
|
+
return template.render(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
def _get_globals(self):
|
|
33
|
+
return {
|
|
34
|
+
'zip': zip,
|
|
35
|
+
'len': len,
|
|
36
|
+
'template_name': self._template_name,
|
|
37
|
+
'now': datetime.now().strftime('%Y-%m-%d %I:%M'),
|
|
38
|
+
'indent': self._indent
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def _indent(self, level):
|
|
42
|
+
return ' ' * level
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
""":mod:`services`
|
|
18
|
+
Defines classes and functions that provide services to the models of the application
|
|
19
|
+
"""
|
|
20
|
+
from .task_runner import TaskRunner
|
|
21
|
+
from .message_board import MessageBoard, MessageArgs, MessageType, MessageResponse
|
|
22
|
+
from .module_service import ModuleService
|
|
23
|
+
from .tool_window_service import ToolWindowService
|
|
24
|
+
from .link_manager import LinkManager, IncompatibleWidgets
|
|
25
|
+
from .email import EmailService
|
|
26
|
+
from .event_registry import EventRegistry, EventTypes
|
|
27
|
+
from .object_converter import ObjectConverter, DictConverter
|
|
28
|
+
from .autosave import Autosave
|
|
29
|
+
from .hotkey_manager import HotkeyManager, HotkeyEvents
|
|
30
|
+
from .theme_manager import ThemeManager
|
|
31
|
+
from .project_updater import ProjectUpdater
|
|
32
|
+
from .file_dialog import FileDialogService
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
from PyQt5.QtCore import QTimer, QObject
|
|
18
|
+
#pylint: enable=no-name-in-module
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Autosave(QObject):
|
|
22
|
+
def __init__(self, save_function, filename, interval=30000):
|
|
23
|
+
QObject.__init__(self)
|
|
24
|
+
self._save_function = save_function
|
|
25
|
+
self._timer = QTimer()
|
|
26
|
+
self._interval = interval
|
|
27
|
+
self._timer.timeout.connect(self._save)
|
|
28
|
+
self._filename = filename
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def filename(self):
|
|
32
|
+
return self._filename
|
|
33
|
+
|
|
34
|
+
def start(self):
|
|
35
|
+
self._timer.start(self._interval)
|
|
36
|
+
|
|
37
|
+
def stop(self):
|
|
38
|
+
self._timer.stop()
|
|
39
|
+
|
|
40
|
+
def _save(self):
|
|
41
|
+
self._save_function(self._filename)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
"""
|
|
18
|
+
http://code.activestate.com/recipes/511443-cross-platform-startfile-and-mailto-functions/
|
|
19
|
+
"""
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
class EmailService:
|
|
23
|
+
def send_mail(self, address, subject, body, attachements=None):
|
|
24
|
+
mailto_string = f'mailto:{address}?subject={subject}&body={body[:2000]}'
|
|
25
|
+
os.startfile(mailto_string)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# pyqttoolkit
|
|
2
|
+
# Copyright (C) 2018-2019, Simmovation Ltd.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, write to the Free Software Foundation,
|
|
16
|
+
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17
|
+
class EventTypes:
|
|
18
|
+
application_closing = 'application_closing'
|
|
19
|
+
opening_project = 'opening_project'
|
|
20
|
+
importing_project = 'importing_project'
|
|
21
|
+
save_requested = 'save_requested'
|
|
22
|
+
close_project_requested = 'close_project_requested'
|
|
23
|
+
project_closed = 'project_closed'
|
|
24
|
+
save_dialog_finished = 'save_dialog_finished'
|
|
25
|
+
|
|
26
|
+
class FutureConnector:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self._handlers = []
|
|
29
|
+
|
|
30
|
+
def connect(self, handler):
|
|
31
|
+
self._handlers.append(handler)
|
|
32
|
+
|
|
33
|
+
def register(self, event):
|
|
34
|
+
for h in self._handlers:
|
|
35
|
+
event.connect(h)
|
|
36
|
+
|
|
37
|
+
class EventRegistry:
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self._events = {}
|
|
40
|
+
self._future_events = {}
|
|
41
|
+
|
|
42
|
+
def register(self, type_, event):
|
|
43
|
+
self._events[type_] = event
|
|
44
|
+
if type_ in self._future_events:
|
|
45
|
+
self._future_events[type_].register(event)
|
|
46
|
+
|
|
47
|
+
def event(self, type_):
|
|
48
|
+
event = self._events.get(type_)
|
|
49
|
+
if not event:
|
|
50
|
+
return self._future_events.setdefault(type_, FutureConnector())
|
|
51
|
+
else:
|
|
52
|
+
return event
|