aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortassaron2017-07-23 17:14:21 -0400
committertassaron2017-07-23 17:14:21 -0400
commitd38109453cea17a31c335837c0029ad51fa3dda1 (patch)
tree57611d2a48795d824972c92756283bf16d44e3fe /src
parentbf0890e7c87c730b8970c1a20c5b6a9a1a55d203 (diff)
better component error messages
fatal errors cancel the export instead of crashing
Diffstat (limited to 'src')
-rw-r--r--src/component.py157
-rw-r--r--src/components/original.py2
-rw-r--r--src/components/sound.py2
-rw-r--r--src/components/video.py24
-rw-r--r--src/core.py10
-rw-r--r--src/mainwindow.py15
-rw-r--r--src/toolkit/common.py8
-rw-r--r--src/toolkit/ffmpeg.py2
-rw-r--r--src/video_thread.py52
9 files changed, 190 insertions, 82 deletions
diff --git a/src/component.py b/src/component.py
index bec2df5..8b5f1b8 100644
--- a/src/component.py
+++ b/src/component.py
@@ -5,13 +5,12 @@
from PyQt5 import uic, QtCore, QtWidgets
import os
-from presetmanager import getPresetDir
-
def commandWrapper(func):
'''Intercepts each component's command() method to check for global args'''
def decorator(self, arg):
if arg.startswith('preset='):
+ from presetmanager import getPresetDir
_, preset = arg.split('=', 1)
path = os.path.join(getPresetDir(self), preset)
if not os.path.exists(path):
@@ -29,6 +28,26 @@ def commandWrapper(func):
return decorator
+def propertiesWrapper(func):
+ '''Intercepts the usual properties if the properties are locked.'''
+ def decorator(self):
+ if self._lockedProperties is not None:
+ return self._lockedProperties
+ else:
+ return func(self)
+ return decorator
+
+
+def errorWrapper(func):
+ '''Intercepts the usual error message if it is locked.'''
+ def decorator(self):
+ if self._lockedError is not None:
+ return self._lockedError
+ else:
+ return func(self)
+ return decorator
+
+
class ComponentMetaclass(type(QtCore.QObject)):
'''
Checks the validity of each Component class imported, and
@@ -37,25 +56,33 @@ class ComponentMetaclass(type(QtCore.QObject)):
'''
def __new__(cls, name, parents, attrs):
if 'ui' not in attrs:
- # use module name as ui filename by default
+ # Use module name as ui filename by default
attrs['ui'] = '%s.ui' % os.path.splitext(
attrs['__module__'].split('.')[-1]
)[0]
- # Turn certain class methods into properties and classmethods
- for key in ('error', 'properties', 'audio'):
- if key not in attrs:
- continue
- attrs[key] = property(attrs[key])
+ # if parents[0] == QtCore.QObject: else:
+ decorate = ('names', 'error', 'audio', 'command', 'properties')
- for key in ('names'):
+ # Auto-decorate methods
+ for key in decorate:
if key not in attrs:
continue
- attrs[key] = classmethod(key)
- # Do not apply these mutations to the base class
- if parents[0] != QtCore.QObject:
- attrs['command'] = commandWrapper(attrs['command'])
+ if key in ('names'):
+ attrs[key] = classmethod(attrs[key])
+
+ if key in ('audio'):
+ attrs[key] = property(attrs[key])
+
+ if key == 'command':
+ attrs[key] = commandWrapper(attrs[key])
+
+ if key == 'properties':
+ attrs[key] = propertiesWrapper(attrs[key])
+
+ if key == 'error':
+ attrs[key] = errorWrapper(attrs[key])
# Turn version string into a number
try:
@@ -83,13 +110,13 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
name = 'Component'
# ui = 'nameOfNonDefaultUiFile'
+
version = '1.0.0'
# The major version (before the first dot) is used to determine
# preset compatibility; the rest is ignored so it can be non-numeric.
modified = QtCore.pyqtSignal(int, dict)
- # ^ Signal used to tell core program that the component state changed,
- # you shouldn't need to use this directly, it is used by self.update()
+ _error = QtCore.pyqtSignal(str, str)
def __init__(self, moduleIndex, compPos, core):
super().__init__()
@@ -100,6 +127,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._trackedWidgets = {}
self._presetNames = {}
+ self._commandArgs = {}
+ self._lockedProperties = None
+ self._lockedError = None
# Stop lengthy processes in response to this variable
self.canceled = False
@@ -127,6 +157,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def error(self):
'''
Return a string containing an error message, or None for a default.
+ Or tuple of two strings for a message with details.
'''
return
@@ -141,12 +172,6 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
https://ffmpeg.org/ffmpeg-filters.html
'''
- def names():
- '''
- Alternative names for renaming a component between project files.
- '''
- return []
-
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
@@ -181,15 +206,29 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for widget in widgets['comboBox']:
widget.currentIndexChanged.connect(self.update)
- def trackWidgets(self, trackDict, presetNames=None):
+ def trackWidgets(self, trackDict, **kwargs):
'''
- Name widgets to track in update(), savePreset(), and loadPreset()
- Accepts a dict with attribute names as keys and widgets as values.
- Optional: a dict of attribute names to map to preset variable names
+ Name widgets to track in update(), savePreset(), loadPreset(), and
+ command(). Requires a dict of attr names as keys, widgets as values
+
+ Optional args:
+ 'presetNames': preset variable names to replace attr names
+ 'commandArgs': arg keywords that differ from attr names
+
+ NOTE: Any kwarg key set to None will selectively disable tracking.
'''
self._trackedWidgets = trackDict
- if type(presetNames) is dict:
- self._presetNames = presetNames
+ for kwarg in kwargs:
+ try:
+ if kwarg in ('presetNames', 'commandArgs'):
+ setattr(self, '_%s' % kwarg, kwargs[kwarg])
+ else:
+ raise BadComponentInit(
+ self,
+ 'Nonsensical keywords to trackWidgets.',
+ immediate=True)
+ except BadComponentInit:
+ continue
def update(self):
'''
@@ -277,6 +316,22 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.commandHelp()
quit(0)
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # "Private" Methods
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+
+ def lockProperties(self, propList):
+ self._lockedProperties = propList
+
+ def lockError(self, msg):
+ self._lockedError = msg
+
+ def unlockProperties(self):
+ self._lockedProperties = None
+
+ def unlockError(self):
+ self._lockedError = None
+
def loadUi(self, filename):
'''Load a Qt Designer ui file to use for this component's widget'''
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
@@ -287,6 +342,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def reset(self):
self.canceled = False
+ self.unlockProperties()
+ self.unlockError()
'''
### Reference methods for creating a new component
@@ -309,16 +366,40 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
-class BadComponentInit(Exception):
+class BadComponentInit(AttributeError):
'''
- General purpose exception that components can raise to indicate
- a Python issue with e.g., dynamic creation of instances or something.
- Decorative for now, may have future use for logging.
+ Indicates a Python error in constructing a component.
+ Raising this locks the component into an error state,
+ and gives the MainWindow a traceback to display.
'''
- def __init__(self, arg, name):
- string = '''################################
-Mandatory argument "%s" not specified
- in %s instance initialization
-###################################'''
- print(string % (arg, name))
- quit()
+ def __init__(self, caller, name, immediate=False):
+ from toolkit import formatTraceback
+ import sys
+ if sys.exc_info()[0] is not None:
+ string = (
+ "%s component's %s encountered %s %s." % (
+ caller.__class__.name,
+ name,
+ 'an' if any([
+ sys.exc_info()[0].__name__.startswith(vowel)
+ for vowel in ('A', 'I')
+ ]) else 'a',
+ sys.exc_info()[0].__name__,
+ )
+ )
+ detail = formatTraceback(sys.exc_info()[2])
+ else:
+ string = name
+ detail = "Methods:\n%s" % (
+ "\n".join(
+ [m for m in dir(caller) if not m.startswith('_')]
+ )
+ )
+
+ if immediate:
+ caller.parent.showMessage(
+ msg=string, detail=detail, icon='Warning'
+ )
+ else:
+ caller.lockProperties(['error'])
+ caller.lockError((string, detail))
diff --git a/src/components/original.py b/src/components/original.py
index 2bda878..570465d 100644
--- a/src/components/original.py
+++ b/src/components/original.py
@@ -15,7 +15,7 @@ class Component(Component):
name = 'Classic Visualizer'
version = '1.0.0'
- def names():
+ def names(*args):
return ['Original Audio Visualization']
def widget(self, *args):
diff --git a/src/components/sound.py b/src/components/sound.py
index dd3cbab..b3a627a 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -18,6 +18,8 @@ class Component(Component):
'chorus': self.page.checkBox_chorus,
'delay': self.page.spinBox_delay,
'volume': self.page.spinBox_volume,
+ }, commandArgs={
+ 'sound': None,
})
def previewRender(self, previewWorker):
diff --git a/src/components/video.py b/src/components/video.py
index 677e3ee..d3696d4 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -14,7 +14,7 @@ from toolkit import openPipe, checkOutput
class Video:
- '''Video Component Frame-Fetcher'''
+ '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
def __init__(self, **kwargs):
mandatoryArgs = [
'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
@@ -28,10 +28,7 @@ class Video:
'component', # component object
]
for arg in mandatoryArgs:
- try:
- setattr(self, arg, kwargs[arg])
- except KeyError:
- raise BadComponentInit(arg, self.__doc__)
+ setattr(self, arg, kwargs[arg])
self.frameNo = -1
self.currentFrame = 'None'
@@ -196,13 +193,16 @@ class Component(Component):
height = int(self.settings.value('outputHeight'))
self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height)
- self.video = Video(
- ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
- width=width, height=height, chunkSize=self.chunkSize,
- frameRate=int(self.settings.value("outputFrameRate")),
- parent=self.parent, loopVideo=self.loopVideo,
- component=self, scale=self.scale
- ) if os.path.exists(self.videoPath) else None
+ try:
+ self.video = Video(
+ ffmpeg=self.core.FFMPEG_BIN, #videoPath=self.videoPath,
+ width=width, height=height, chunkSize=self.chunkSize,
+ frameRate=int(self.settings.value("outputFrameRate")),
+ parent=self.parent, loopVideo=self.loopVideo,
+ component=self, scale=self.scale
+ ) if os.path.exists(self.videoPath) else None
+ except KeyError:
+ raise BadComponentInit(self, 'Frame Fetcher initialization')
def frameRender(self, layerNo, frameNo):
if self.video:
diff --git a/src/core.py b/src/core.py
index eb6398b..2f9c36c 100644
--- a/src/core.py
+++ b/src/core.py
@@ -22,13 +22,12 @@ class Core:
'''
def __init__(self):
- self.findComponents()
+ self.importComponents()
self.selectedComponents = []
self.savedPresets = {} # copies of presets to detect modification
self.openingProject = False
- def findComponents(self):
- '''Imports all the component modules'''
+ def importComponents(self):
def findComponents():
for f in os.listdir(Core.componentsPath):
name, ext = os.path.splitext(f)
@@ -225,9 +224,8 @@ class Core:
return
if hasattr(loader, 'createNewProject'):
loader.createNewProject(prompt=False)
- import traceback
- msg = '%s: %s\n\nTraceback:\n' % (typ.__name__, value)
- msg += "\n".join(traceback.format_tb(tb))
+ msg = '%s: %s\n\n' % (typ.__name__, value)
+ msg += toolkit.formatTraceback(tb)
loader.showMessage(
msg="Project file '%s' is corrupted." % filepath,
showCancel=False,
diff --git a/src/mainwindow.py b/src/mainwindow.py
index f333513..a32c1b4 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -571,6 +571,15 @@ class MainWindow(QtWidgets.QMainWindow):
self.videoWorker.encoding.connect(self.changeEncodingStatus)
self.createVideo.emit()
+ @QtCore.pyqtSlot(str, str)
+ def videoThreadError(self, msg, detail):
+ self.showMessage(
+ msg=msg,
+ detail=detail,
+ icon='Warning',
+ )
+ self.stopVideo()
+
def changeEncodingStatus(self, status):
self.encoding = status
if status:
@@ -675,6 +684,8 @@ class MainWindow(QtWidgets.QMainWindow):
# connect to signal that adds an asterisk when modified
self.core.selectedComponents[index].modified.connect(
self.updateComponentTitle)
+ self.core.selectedComponents[index]._error.connect(
+ self.videoThreadError)
self.pages.insert(index, self.core.selectedComponents[index].page)
stackedWidget.insertWidget(index, self.pages[index])
@@ -751,7 +762,7 @@ class MainWindow(QtWidgets.QMainWindow):
if mousePos > -1:
change = (componentList.currentRow() - mousePos) * -1
else:
- change = (componentList.count() - componentList.currentRow() -1)
+ change = (componentList.count() - componentList.currentRow() - 1)
self.moveComponent(change)
def changeComponentWidget(self):
@@ -936,7 +947,7 @@ class MainWindow(QtWidgets.QMainWindow):
if event.type() == QtCore.QEvent.WindowActivate \
or event.type() == QtCore.QEvent.FocusIn:
Core.windowHasFocus = True
- elif event.type()== QtCore.QEvent.WindowDeactivate \
+ elif event.type() == QtCore.QEvent.WindowDeactivate \
or event.type() == QtCore.QEvent.FocusOut:
Core.windowHasFocus = False
return False
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 5fe601f..251a2c1 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -107,3 +107,11 @@ def rgbFromString(string):
return tup
except:
return (255, 255, 255)
+
+
+def formatTraceback(tb=None):
+ import traceback
+ if tb is None:
+ import sys
+ tb = sys.exc_info()[2]
+ return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb))
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index 30dc0b3..8f5ae87 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -103,7 +103,7 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
globalFilters = 0 # increase to add global filters
extraAudio = [
comp.audio for comp in components
- if 'audio' in comp.properties
+ if 'audio' in comp.properties()
]
if extraAudio or globalFilters > 0:
# Add -i options for extra input files
diff --git a/src/video_thread.py b/src/video_thread.py
index 7fe3e02..68eae4f 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -18,7 +18,7 @@ from threading import Thread, Event
import time
import signal
-import core
+from component import BadComponentInit
from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard
@@ -105,8 +105,7 @@ class Worker(QtCore.QObject):
while not self.stopped:
audioI, frame = self.previewQueue.get()
- if core.Core.windowHasFocus \
- and time.time() - self.lastPreview >= 0.06 or audioI == 0:
+ if time.time() - self.lastPreview >= 0.06 or audioI == 0:
image = Image.alpha_composite(background.copy(), frame)
self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
self.lastPreview = time.time()
@@ -153,39 +152,48 @@ class Worker(QtCore.QObject):
]))
self.staticComponents = {}
for compNo, comp in enumerate(reversed(self.components)):
- comp.preFrameRender(
- worker=self,
- completeAudioArray=self.completeAudioArray,
- sampleSize=self.sampleSize,
- progressBarUpdate=self.progressBarUpdate,
- progressBarSetText=self.progressBarSetText
- )
+ try:
+ comp.preFrameRender(
+ worker=self,
+ completeAudioArray=self.completeAudioArray,
+ sampleSize=self.sampleSize,
+ progressBarUpdate=self.progressBarUpdate,
+ progressBarSetText=self.progressBarSetText
+ )
+ except BadComponentInit:
+ pass
- if 'error' in comp.properties:
+ if 'error' in comp.properties():
self.cancel()
self.canceled = True
canceledByComponent = True
- errMsg = "Component #%s encountered an error!" % compNo \
- if comp.error is None else 'Component #%s (%s): %s' % (
+ compError = comp.error() \
+ if type(comp.error()) is tuple else (comp.error(), '')
+ errMsg = (
+ "Component #%s encountered an error!" % compNo
+ if comp.error() is None else
+ 'Export cancelled by component #%s (%s): %s' % (
str(compNo),
str(comp),
- comp.error
- )
- self.parent.showMessage(
- msg=errMsg,
- icon='Warning',
- parent=None # MainWindow is in a different thread
+ compError[0]
)
+ )
+ comp._error.emit(errMsg, compError[1])
break
- if 'static' in comp.properties:
+ if 'static' in comp.properties():
self.staticComponents[compNo] = \
comp.frameRender(compNo, 0).copy()
if self.canceled:
if canceledByComponent:
print('Export cancelled by component #%s (%s): %s' % (
- compNo, str(comp), comp.error
- ))
+ compNo,
+ comp.name,
+ 'No message.' if comp.error() is None else (
+ comp.error() if type(comp.error()) is str
+ else comp.error()[0])
+ )
+ )
self.cancelExport()
return