#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: kushal
GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
"""
from PyQt5 import QtCore, QtGui, QtWidgets
import inspect
from typing import *
from collections import namedtuple
from functools import partial
from .argument import Arg, ArgNumeric
def _get_argument(sig: inspect.Parameter, parent, vlayout, **opts):
if sig.default is inspect._empty:
default = None
else:
default = sig.default
kwargs = dict(
name=sig.name,
typ=sig.annotation,
val=default,
parent=parent,
vlayout=vlayout
)
kwargs.update(opts)
if sig.annotation in [int, float]:
return ArgNumeric(**kwargs)
else:
return Arg(**kwargs)
# this is a massive nested lambda, not sure if there's a more elegant way to do this without a nasty loop
_ignore_arguments = lambda d: (lambda d: True if d['ignore'] else False)(d) if 'ignore' in d.keys() else False
[docs]class Function(QtCore.QObject):
# emit the entire dict
sig_changed = QtCore.pyqtSignal(dict)
sig_set_clicked = QtCore.pyqtSignal(dict)
# emits the name of the arg and its value
sig_arg_changed = QtCore.pyqtSignal(str, object)
[docs] def __init__(
self,
func: callable,
arg_opts: dict = None,
parent: Optional[QtWidgets.QWidget] = None,
kwarg_entry: bool = False,
):
"""
Creates a widget based on the function signature
Parameters
----------
func : callable
A function with type annotations
arg_opts : dict
manually set certain features of an Arg
parent : Optional[QtWidgets.QWidget]
parent QWidget
kwarg_entry : bool
Not yet implemented.
include a text box for kwargs entry
Attributes
-------
sig_changed : dict
Emitted when an argument value changes.
Emits dict for all function arguments.
See ``get_data()`` for details on the dict.
sig_set_clicked : dict
Emitted when the "Set" button is clicked.
Emits dict for all function arguments.
See ``get_data()`` for details on the dict.
sig_arg_changed : str, object
Emitted when specific argument value changes.
Emits argument name and argument value
Examples
--------
**Basic**
.. code-block:: python
:linenos:
from PyQt5 import QtWidgets
from qtap import Function
# annotated function
def f(a: int = 1, b: float = 3.14, c: str = 'yay', d: bool = True):
pass
app = QtWidgets.QApplication([])
# basic
func = Function(f)
func.widget.show()
app.exec()
**Opt Args**
.. code-block:: python
:linenos:
from PyQt5 import QtWidgets
from qtap import Function
from pyqtgraph.console import ConsoleWidget
def f(a: int = 1, b: float = 3.14, c: str = 'yay', d: bool = True):
pass
if __name__ == '__main__':
app = QtWidgets.QApplication([])
# opt args dict
opts = \
{
'b':
{
'use_slider': True,
'minmax': (0, 100),
'step': 1,
'suffix': '%',
'typ': int,
'tooltip': 'yay tooltips'
}
}
func = Function(f, arg_opts=opts)
func.widget.show()
console = ConsoleWidget(parent=func.widget, namespace={'this': func})
func.vlayout.addWidget(console)
app.exec()
"""
super(Function, self).__init__(parent)
self.widget = QtWidgets.QWidget(parent)
self.vlayout = QtWidgets.QVBoxLayout(self.widget)
self.callable = func
self.name = self.callable.__name__
self._qlabel = QtWidgets.QLabel(self.widget)
self._qlabel.setStyleSheet("font-weight: bold")
self._qlabel.setText(self.name)
self.vlayout.addWidget(self._qlabel)
arg_names = inspect.signature(func).parameters.keys()
arg_sigs = inspect.signature(func).parameters.values()
self.arg_opts = dict.fromkeys(arg_names)
self.arg_opts = {arg: {} for arg in arg_names}
if arg_opts is not None:
self.arg_opts.update(arg_opts)
ignore = [
k for k in self.arg_opts.keys() if _ignore_arguments(self.arg_opts[k])
]
arg_names = [n for n in arg_names if n not in ignore]
# Add all the arguments as named tuples
# so they're accessible like attributes
# dynamically named based on the args from the function!
Arguments = namedtuple("Arguments", arg_names)
self.arguments = Arguments(
*(
_get_argument(
sig,
parent=self.widget,
vlayout=self.vlayout,
**self.arg_opts[sig.name]
)
for sig in arg_sigs if sig.name not in ignore
)
)
# button at the bottom, sends a "set" signal when clicked
self.button_set = QtWidgets.QPushButton(self.widget)
self.button_set.setText('Set')
self.vlayout.addWidget(self.button_set)
self.button_set.clicked.connect(
partial(self._emit_data, self.sig_set_clicked)
)
for arg in self.arguments:
# emit entire dict when arg is changed
arg.sig_changed.connect(
partial(self._emit_data, self.sig_changed)
)
# also emit just arg.name and arg.val when changed
arg.sig_changed.connect(
partial(self.sig_arg_changed.emit, arg.name)
)
def _emit_data(self, sig: QtCore.pyqtBoundSignal):
sig.emit(self.get_data())
[docs] def get_data(self) -> Dict[str, object]:
"""
Get the data from the function arguments
Returns
-------
dict
dict keys are the argument names, dict values are the argument vals
"""
return {arg.name: arg.val for arg in self.arguments}
def set_data(self, d: dict):
for arg in d.keys():
getattr(self.arguments, arg).val = d[arg]
def set_title(self, title: str):
"""
Set the title text for the function. The default title is the function name.
Parameters
----------
title : str
Title to display above the widgets for this function
Returns
-------
None
"""
self._qlabel.setText(title)
def __repr__(self):
return f'{self.name}' \
f'\n' + \
'\n'.join(
[
f' {arg.name}:\n' \
f' {arg.typ}\n' \
f' {arg.val}'
for arg in self.arguments
]
)
[docs]class Functions(QtWidgets.QWidget):
sig_changed = QtCore.pyqtSignal(dict)
sig_set_clicked = QtCore.pyqtSignal(dict)
[docs] def __init__(
self,
functions: List[callable],
arg_opts: Optional[List[dict]] = None,
parent: Optional[QtWidgets.QWidget] = None,
scroll: bool = False,
orient: str = 'V',
columns: bool = False,
**kwargs
):
"""
Parameters
----------
functions : List[callable]
list of functions
arg_opts : List[dict], optional
optional list of dicts to manually set features of an argument.
passed to ``Function``
parent : QtWidgets.QWidget, optional
parent widget
scroll : bool
Not yet implemented
orient : str
orientation of the individual functions. One of ``V`` or ``H``.
Default orientation is ``V`` (vertical)
columns : bool
Not yet implemented
**kwargs
passed to QtWidgets.QWidget.__init__()
Attributes
-------
sig_changed : dict
Emitted when an underlying function emits sig_changed().
The emitted dict comes from ``get_data()``,
see the docstring for ``get_data()`` for details.
sig_set_clicked : dict
Emitted when an underlying function emits sig_set_clicked().
The emitted dict comes from ``get_data()``,
see the docstring for ``get_data()`` for details.
Examples
--------
**Basic**
.. code-block:: python
:linenos:
from PyQt5 import QtWidgets
from qtap import Functions
from pyqtgraph.console import ConsoleWidget
def func_A(a: int = 1, b: float = 3.14, c: str = 'yay', d: bool = True):
pass
def func_B(x: float = 50, y: int = 2.7, u: str = 'bah'):
pass
if __name__ == '__main__':
app = QtWidgets.QApplication([])
functions = Functions([func_A, func_B])
console = ConsoleWidget(parent=functions, namespace={'this': functions})
functions.main_layout.addWidget(console)
functions.show()
app.exec()
**Opt Args**
.. code-block:: python
:linenos:
from PyQt5 import QtWidgets
from qtap import Functions
from pyqtgraph.console import ConsoleWidget
def func_A(a: int = 1, b: float = 3.14, c: str = 'yay', d: bool = True):
pass
def func_B(x: float = 50, y: int = 2.7, u: str = 'bah'):
pass
if __name__ == '__main__':
app = QtWidgets.QApplication([])
# opt args for ``func_A``
opts_A = \
{
'b':
{
'use_slider': True,
'minmax': (0, 100),
'step': 1,
'suffix': '%',
'typ': int,
'tooltip': 'yay tooltips'
}
}
# functions where one has ``opt_args``
functions = Functions(
functions=[func_A, func_B],
arg_opts=[opts_A, None], # opt_args in same order as functions
)
console = ConsoleWidget(parent=functions, namespace={'this': functions})
functions.main_layout.addWidget(console)
functions.show()
app.exec()
"""
super().__init__(parent, **kwargs)
_functions = namedtuple(
'Functions',
[f.__name__ for f in functions]
)
if arg_opts is None:
arg_opts = [None] * len(functions)
self.functions = _functions(
*(
Function(
func,
opt,
parent=self
)
for func, opt in zip(functions, arg_opts)
)
)
if scroll:
self.vlayout = QtWidgets.QVBoxLayout(self)
self.scroll_area = QtWidgets.QScrollArea(self)
self.vlayout.addWidget(self.scroll_area)
self.scroll_area.setWidgetResizable(True)
self.scroll_content = QtWidgets.QWidget(self.scroll_area)
self.scroll_layout = QtWidgets.QVBoxLayout(self.scroll_content)
self.scroll_content.setLayout(self.scroll_layout)
self.main_layout = self.scroll_layout
else:
if orient in ['V', 'vertical']:
self.main_layout = QtWidgets.QVBoxLayout(self)
elif orient in ['H', 'horizontal']:
self.main_layout = QtWidgets.QHBoxLayout(self)
f: Function
for f in self.functions:
self.main_layout.addWidget(f.widget)
# emit dict when any function changes
f.sig_changed.connect(
partial(self._emit_data, self.sig_changed)
)
# emit dict when any function is set
f.sig_set_clicked.connect(
partial(self._emit_data, self.sig_set_clicked)
)
def _emit_data(self, sig):
sig.emit(self.get_data())
[docs] def get_data(self) -> Dict[callable, dict]:
"""
Returns
-------
dict
dict keys are the functions, each dict values is a kwargs dict
"""
return {f.callable: f.get_data() for f in self.functions}
def __repr__(self):
return '\n'.join(
[
f.__repr__() for f in self.functions
]
)