From f03a3a686c7304588dd434322c73506531e53595 Mon Sep 17 00:00:00 2001 From: Brianna Rainey Date: Thu, 12 Feb 2026 15:38:54 -0500 Subject: v2.2.4 - Quiet FFmpeg; add "invert" option to Classic Vis; fix CLI parsing for Image component (#96) * change noisiness of terminal output ffmpeg no longer prints everything into the terminal unless we're in `--verbose` mode. percentage progress text stays on one line while not in verbose mode. * Added hint to run `avp --verbose` if `avp --log` is run with no avp_debug.log file present * Classic Visualizer: add invert option * Image component: fix path commandline option * Image component: restrict file formats in CLI to match GUI * Color component: add tooltip to color2 picker (second color of gradients) * change tests to work with pytest-xdist avp core stores its config (location of `settings.ini`) in temp directories if using multiple workers to run tests, so they don't interfere with each other. when using a single worker, the `tests/data/config` directory is still used * check alt comp names when parsing cmdline * rename `original.py` to `classic.py` * move `component.py` into subpackage * rename comp_original to comp_classic * show traceback if renderFrame() raises exception * do not try to insert non-existent components from project files * add "composite" property for components if a component returns "composite" then it will receive a frame to draw on during calls to previewRender and frameRender * more tests of projects, actions, waveform, spectrum, image, color, classic * do not change presetDir to "projects" within PresetManager--- src/avp/libcomponent/metaclass.py | 257 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 src/avp/libcomponent/metaclass.py (limited to 'src/avp/libcomponent/metaclass.py') diff --git a/src/avp/libcomponent/metaclass.py b/src/avp/libcomponent/metaclass.py new file mode 100644 index 0000000..e8ad949 --- /dev/null +++ b/src/avp/libcomponent/metaclass.py @@ -0,0 +1,257 @@ +import os +import logging +from PyQt6 import QtCore + +from .exceptions import ComponentError +from ..toolkit import connectWidget +from ..toolkit.frame import BlankFrame + +log = logging.getLogger("AVP.ComponentHandler") + + +class ComponentMetaclass(type(QtCore.QObject)): + """ + Checks the validity of each Component class and mutates some attrs. + E.g., takes only major version from version string & decorates methods + """ + + def initializationWrapper(func): + def initializationWrapper(self, *args, **kwargs): + try: + return func(self, *args, **kwargs) + except Exception: + try: + raise ComponentError(self, "initialization process") + except ComponentError: + return + + return initializationWrapper + + def renderWrapper(func): + def renderWrapper(self, *args, **kwargs): + try: + log.verbose( + "### %s #%s renders a preview frame ###", + self.__class__.name, + str(self.compPos), + ) + return func(self, *args, **kwargs) + except Exception as e: + try: + if e.__class__.__name__.startswith("Component"): + raise + else: + raise ComponentError(self, "renderer") + except ComponentError: + return BlankFrame() + + return renderWrapper + + def commandWrapper(func): + """Intercepts the command() method to check for global args""" + + def commandWrapper(self, arg): + if arg.startswith("preset="): + _, preset = arg.split("=", 1) + path = os.path.join(self.core.getPresetDir(self), preset) + if not os.path.exists(path): + print('Couldn\'t locate preset "%s"' % preset) + quit(1) + else: + print('Opening "%s" preset on layer %s' % (preset, self.compPos)) + self.core.openPreset(path, self.compPos, preset) + # Don't call the component's command() method + return + else: + return func(self, arg) + + return commandWrapper + + def propertiesWrapper(func): + """Intercepts the usual properties if the properties are locked.""" + + def propertiesWrapper(self): + if self._lockedProperties is not None: + return self._lockedProperties + else: + try: + return func(self) + except Exception: + try: + raise ComponentError(self, "properties") + except ComponentError: + return [] + + return propertiesWrapper + + def errorWrapper(func): + """Intercepts the usual error message if it is locked.""" + + def errorWrapper(self): + if self._lockedError is not None: + return self._lockedError + else: + return func(self) + + return errorWrapper + + def loadPresetWrapper(func): + """Wraps loadPreset to handle the self.openingPreset boolean""" + + class openingPreset: + def __init__(self, comp): + self.comp = comp + + def __enter__(self): + self.comp.openingPreset = True + + def __exit__(self, *args): + self.comp.openingPreset = False + + def presetWrapper(self, *args): + with openingPreset(self): + try: + return func(self, *args) + except Exception: + try: + raise ComponentError(self, "preset loader") + except ComponentError: + return + + return presetWrapper + + def updateWrapper(func): + """ + Calls _preUpdate before every subclass update(). + Afterwards, for non-user updates, calls _autoUpdate(). + For undoable updates triggered by the user, calls _userUpdate() + """ + + class wrap: + def __init__(self, comp, auto): + self.comp = comp + self.auto = auto + + def __enter__(self): + self.comp._preUpdate() + + def __exit__(self, *args): + if ( + self.auto + or self.comp.openingPreset + or not hasattr(self.comp.parent, "undoStack") + ): + log.verbose("Automatic update") + self.comp._autoUpdate() + else: + log.verbose("User update") + self.comp._userUpdate() + + def updateWrapper(self, **kwargs): + auto = kwargs["auto"] if "auto" in kwargs else False + with wrap(self, auto): + try: + return func(self) + except Exception: + try: + raise ComponentError(self, "update method") + except ComponentError: + return + + return updateWrapper + + def widgetWrapper(func): + """Connects all widgets to update method after the subclass's method""" + + class wrap: + def __init__(self, comp): + self.comp = comp + + def __enter__(self): + pass + + def __exit__(self, *args): + for widgetList in self.comp._allWidgets.values(): + for widget in widgetList: + log.verbose("Connecting %s", str(widget.__class__.__name__)) + connectWidget(widget, self.comp.update) + + def widgetWrapper(self, *args, **kwargs): + auto = kwargs["auto"] if "auto" in kwargs else False + with wrap(self): + try: + return func(self, *args, **kwargs) + except Exception: + try: + raise ComponentError(self, "widget creation") + except ComponentError: + return + + return widgetWrapper + + def __new__(cls, name, parents, attrs): + if "ui" not in attrs: + # Use module name as ui filename by default + attrs["ui"] = ( + "%s.ui" % os.path.splitext(attrs["__module__"].split(".")[-1])[0] + ) + + decorate = ( + "names", # Class methods + "error", + "audio", + "properties", # Properties + "preFrameRender", + "previewRender", + "loadPreset", + "command", + "update", + "widget", + ) + + # Auto-decorate methods + for key in decorate: + if key not in attrs: + continue + if key in ("names"): + attrs[key] = classmethod(attrs[key]) + elif key in ("audio"): + attrs[key] = property(attrs[key]) + elif key == "command": + attrs[key] = cls.commandWrapper(attrs[key]) + elif key == "previewRender": + attrs[key] = cls.renderWrapper(attrs[key]) + elif key == "preFrameRender": + attrs[key] = cls.initializationWrapper(attrs[key]) + elif key == "properties": + attrs[key] = cls.propertiesWrapper(attrs[key]) + elif key == "error": + attrs[key] = cls.errorWrapper(attrs[key]) + elif key == "loadPreset": + attrs[key] = cls.loadPresetWrapper(attrs[key]) + elif key == "update": + attrs[key] = cls.updateWrapper(attrs[key]) + elif key == "widget" and parents[0] != QtCore.QObject: + attrs[key] = cls.widgetWrapper(attrs[key]) + + # Turn version string into a number + try: + if "version" not in attrs: + log.error( + "No version attribute in %s. Defaulting to 1", + attrs["name"], + ) + attrs["version"] = 1 + else: + attrs["version"] = int(attrs["version"].split(".")[0]) + except ValueError: + log.critical( + "%s component has an invalid version string:\n%s", + attrs["name"], + str(attrs["version"]), + ) + except KeyError: + log.critical("%s component has no version string.", attrs["name"]) + else: + return super().__new__(cls, name, parents, attrs) + quit(1) -- cgit v1.2.3