triplot 1.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.
dscpanel/ui/loading.py ADDED
@@ -0,0 +1,95 @@
1
+ """Reading files without freezing the window.
2
+
3
+ Measured on real files: 1.09 s for a 16 MB `.tri` (seven segments,
4
+ 13 200 points each, eighteen stored analyses) and 0.51 s for an 11 MB one. A
5
+ drop of five files is therefore several seconds, and several seconds of a
6
+ frozen window is a program that looks broken.
7
+
8
+ So every read runs on a thread pool and the window is told as each one
9
+ arrives. Two things this deliberately does NOT do: it does not parse on the
10
+ GUI thread "just for one small file" (the small file is the one that turns
11
+ out to be 40 MB), and it does not hand a half-built sample to the window
12
+ (the reader either returns a `Sample` or raises, and the failure carries the
13
+ file's name so a bad file in a drop of five costs one line and not the drop).
14
+ """
15
+
16
+ from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal
17
+
18
+ from ..core import loader
19
+
20
+
21
+ class _JobSignals(QObject):
22
+ done = Signal(object, str, str) # sample, path, error
23
+
24
+
25
+ class _Job(QRunnable):
26
+ def __init__(self, path, signals):
27
+ QRunnable.__init__(self)
28
+ self.path = str(path)
29
+ self.signals = signals
30
+
31
+ def run(self):
32
+ try:
33
+ sample = loader.read_sample(self.path)
34
+ except Exception as exc: # the reader raises many kinds
35
+ self.signals.done.emit(None, self.path, str(exc))
36
+ return
37
+ self.signals.done.emit(sample, self.path, "")
38
+
39
+
40
+ class Loader(QObject):
41
+ """Reads files in the background and reports each one as it lands."""
42
+
43
+ loaded = Signal(object) # Sample
44
+ failed = Signal(str, str) # path, message
45
+ progress = Signal(int, int) # done, total
46
+ finished = Signal()
47
+
48
+ def __init__(self, parent=None):
49
+ QObject.__init__(self, parent)
50
+ self._pool = QThreadPool(self)
51
+ # Two at a time. The reader is numpy-heavy and mostly holds the GIL,
52
+ # so more threads would not read faster, and a bounded pool keeps a
53
+ # drop of twenty files from starting twenty reads at once.
54
+ self._pool.setMaxThreadCount(2)
55
+ self._signals = _JobSignals()
56
+ self._signals.done.connect(self._one_done)
57
+ self._pending = 0
58
+ self._total = 0
59
+
60
+ def busy(self):
61
+ return self._pending > 0
62
+
63
+ def load(self, paths):
64
+ """Queue every readable path. Returns how many were queued."""
65
+ queued = 0
66
+ for path in paths or ():
67
+ if not loader.looks_readable(path):
68
+ continue
69
+ self._pending += 1
70
+ self._total += 1
71
+ self._pool.start(_Job(path, self._signals))
72
+ queued += 1
73
+ if queued:
74
+ self.progress.emit(self._total - self._pending, self._total)
75
+ return queued
76
+
77
+ def wait(self, timeout_ms=30000):
78
+ """For tests: block until the queue is empty."""
79
+ return self._pool.waitForDone(timeout_ms)
80
+
81
+ def _one_done(self, sample, path, error):
82
+ self._pending = max(0, self._pending - 1)
83
+ if sample is None:
84
+ self.failed.emit(path, error)
85
+ else:
86
+ self.loaded.emit(sample)
87
+ self.progress.emit(self._total - self._pending, self._total)
88
+ if not self._pending:
89
+ self._total = 0
90
+ self.finished.emit()
91
+
92
+ # Qt calls the handlers here by itself; an error in one is logged and
93
+ # survived rather than the end of the program (`core/log.py`).
94
+ from ..core import log as _log
95
+ _log.guard_classes(globals(), __name__)
dscpanel/ui/numbox.py ADDED
@@ -0,0 +1,103 @@
1
+ """Number boxes that take a sum as well as a number.
2
+
3
+ "255-20" in a box is 235. Every number box of the program is one of these
4
+ (`NumberBox`, `WholeBox`).
5
+ """
6
+
7
+ from PySide6.QtCore import QLocale
8
+ from PySide6.QtGui import QValidator
9
+ from PySide6.QtWidgets import QDoubleSpinBox, QSpinBox
10
+
11
+ from ..core import numbers
12
+
13
+
14
+ class _Sums(object):
15
+ """A spin box that takes a SUM as well as a number: "255-20" is 235
16
+ worked out on Enter or when the box is left.
17
+ `core.numbers.evaluate`: + - * / and brackets, a comma is a decimal
18
+ point. A sum past the range is clamped to it."""
19
+
20
+ def _setup(self):
21
+ locale = QLocale(QLocale.C)
22
+ locale.setNumberOptions(QLocale.OmitGroupSeparator)
23
+ self.setLocale(locale)
24
+ self.setKeyboardTracking(False)
25
+
26
+ def _core(self, text):
27
+ """The typed text without the box's prefix and suffix."""
28
+ text = str(text).strip()
29
+ suffix, prefix = self.suffix().strip(), self.prefix().strip()
30
+ if suffix and text.endswith(suffix):
31
+ text = text[:-len(suffix)]
32
+ if prefix and text.startswith(prefix):
33
+ text = text[len(prefix):]
34
+ return text.strip()
35
+
36
+ def _sum(self, text):
37
+ """The value of a typed sum, clamped; None if it is not one."""
38
+ core = self._core(text)
39
+ if not numbers.is_sum(core):
40
+ return None
41
+ value = numbers.evaluate(core)
42
+ if value is None:
43
+ return None
44
+ return min(max(value, self.minimum()), self.maximum())
45
+
46
+ def validate(self, text, pos):
47
+ core = self._core(text)
48
+ if numbers.is_sum(core):
49
+ # Still being typed ("255-") is fine; Enter works it out.
50
+ state = (QValidator.Acceptable
51
+ if numbers.evaluate(core) is not None
52
+ else QValidator.Intermediate)
53
+ return state, text, pos
54
+ state, _fixed, position = self._base.validate(
55
+ self, self._normalise(text), pos)
56
+ return state, text, position
57
+
58
+ def valueFromText(self, text):
59
+ value = self._sum(text)
60
+ if value is not None:
61
+ return self._cast(value)
62
+ return self._base.valueFromText(self, self._normalise(text))
63
+
64
+ def fixup(self, text):
65
+ value = self._sum(text)
66
+ if value is not None:
67
+ return self.textFromValue(self._cast(value))
68
+ return text
69
+
70
+ @staticmethod
71
+ def _normalise(text):
72
+ return str(text).strip().replace(",", ".")
73
+
74
+
75
+ class NumberBox(_Sums, QDoubleSpinBox):
76
+ """A spin box that reads "1.5" and "1,5" alike, takes a sum, and
77
+ commits on Enter."""
78
+
79
+ _base = QDoubleSpinBox
80
+ _cast = float
81
+
82
+ def __init__(self, parent=None):
83
+ QDoubleSpinBox.__init__(self, parent)
84
+ self._setup()
85
+
86
+
87
+ class WholeBox(_Sums, QSpinBox):
88
+ """`NumberBox` for whole numbers: a sum is rounded."""
89
+
90
+ _base = QSpinBox
91
+
92
+ def __init__(self, parent=None):
93
+ QSpinBox.__init__(self, parent)
94
+ self._setup()
95
+
96
+ @staticmethod
97
+ def _cast(value):
98
+ return int(round(value))
99
+
100
+ # Qt calls the handlers here by itself; an error in one is logged and
101
+ # survived rather than the end of the program (`core/log.py`).
102
+ from ..core import log as _log
103
+ _log.guard_classes(globals(), __name__)