Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion examples/widget_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,36 @@ def widget_demo(
datetime=datetime.datetime.now(),
filename=Path.home(),
):
"""Run some computation."""
"""We can use numpy docstrings to provide tooltips

Parameters
----------
boolean : bool, optional
A checkbox for booleans, by default True
integer : int, optional
Some integer, by default 1
spin_float : float, optional
This one is a float, by default "pi"
slider_float : float, optional
Hey look! I'm a slider, by default 4.5
string : str, optional
We'll use this string carefully, by default "Text goes here"
dropdown : Enum, optional
Pick a medium, by default Medium.Glass
date : datetime.date, optional
Your birthday, by default datetime.date(1999, 12, 31)
time : datetime.time, optional
Some time, by default datetime.time(1, 30, 20)
datetime : datetime.datetime, optional
A very specific time and date, by default datetime.datetime.now()
filename : str, optional
Pick a path, by default Path.home()

Returns
-------
[type]
[description]
"""
return locals().values()


Expand Down
49 changes: 0 additions & 49 deletions magicgui/_parse.py

This file was deleted.

6 changes: 6 additions & 0 deletions magicgui/backends/_qtpy/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ def _mgui_set_min_width(self, value) -> None:
self._qwidget.setMinimumWidth(value)
self._qwidget.resize(self._qwidget.sizeHint())

def _mgui_get_tooltip(self) -> str:
return self._qwidget.toolTip()

def _mgui_set_tooltip(self, value: Optional[str]) -> None:
self._qwidget.setToolTip(str(value) if value else None)

def _mgui_bind_parent_change_callback(self, callback):
self._event_filter.parentChanged.connect(callback)

Expand Down
42 changes: 40 additions & 2 deletions magicgui/function_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
from __future__ import annotations

import inspect
import re
import warnings
from typing import Any, Callable, Dict, Optional, TypeVar, Union, overload

from docstring_parser import parse

from magicgui.application import AppRef
from magicgui.events import EventEmitter
from magicgui.signature import magic_signature
Expand All @@ -16,6 +19,25 @@
from magicgui.widgets._protocols import ContainerProtocol


def _inject_tooltips_from_docstrings(
docstring: Optional[str], param_options: Dict[str, dict]
):
"""Update ``param_options`` dict with tooltips extracted from ``docstring``."""
if not docstring:
return
for param in parse(docstring).params:
# make the tooltip from the first sentence in the param doc description
tooltip = param.description.split(".", maxsplit=1)[0]
tooltip = re.split(r",?\s?([bB]y )?[dD]efault", tooltip)[0]
# this is to catch potentially bad arg_name parsing in docstring_parser
# if using napoleon style google docstringss
argname = param.arg_name.split(" ", maxsplit=1)[0]
if argname not in param_options:
param_options[argname] = {}
# use setdefault so as not to override an explicitly provided tooltip
param_options[argname].setdefault("tooltip", tooltip)


class FunctionGui(Container):
"""Wrapper for a container of widgets representing a callable object.

Expand All @@ -31,6 +53,8 @@ class FunctionGui(Container):
by default "horizontal".
labels : bool, optional
Whether labels are shown in the widget. by default True
tooltips : bool, optional
Whether tooltips are shown when hovering over widgets. by default True
app : magicgui.Application or str, optional
A backend to use, by default ``None`` (use the default backend.)
show : bool, optional
Expand Down Expand Up @@ -60,12 +84,13 @@ def __init__(
function: Callable,
call_button: Union[bool, str] = False,
layout: str = "horizontal",
labels=True,
labels: bool = True,
tooltips: bool = True,
app: AppRef = None,
show: bool = False,
auto_call: bool = False,
result_widget: bool = False,
param_options: Optional[dict] = None,
param_options: Optional[Dict[str, dict]] = None,
name: str = None,
**kwargs,
):
Expand All @@ -74,6 +99,15 @@ def __init__(
if extra:
s = "s" if len(extra) > 1 else ""
raise TypeError(f"FunctionGui got unexpected keyword argument{s}: {extra}")
if param_options is None:
param_options = {}
elif not isinstance(param_options, dict) or not all(
isinstance(x, dict) for x in param_options.values()
):
raise TypeError("'param_options' must be a dict of dicts")
if tooltips:
_inject_tooltips_from_docstrings(function.__doc__, param_options)

self._function = function
sig = magic_signature(function, gui_options=param_options)
super().__init__(
Expand Down Expand Up @@ -286,6 +320,7 @@ def magicgui(
*,
layout: str = "horizontal",
labels: bool = True,
tooltips: bool = True,
call_button: Union[bool, str] = False,
auto_call: bool = False,
result_widget: bool = False,
Expand All @@ -304,6 +339,8 @@ def magicgui(
by default "horizontal".
labels : bool, optional
Whether labels are shown in the widget. by default True
tooltips : bool, optional
Whether tooltips are shown when hovering over widgets. by default True
call_button : bool or str, optional
If ``True``, create an additional button that calls the original function when
clicked. If a ``str``, set the button text. by default False
Expand Down Expand Up @@ -357,6 +394,7 @@ def inner_func(func: Callable) -> FunctionGui:
call_button=call_button,
layout=layout,
labels=labels,
tooltips=tooltips,
param_options=param_options,
auto_call=auto_call,
result_widget=result_widget,
Expand Down
1 change: 1 addition & 0 deletions magicgui/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ class WidgetOptions(TypedDict, total=False):
layout: str # for things like containers
orientation: str # for things like sliders
mode: Union[str, FileDialogMode]
tooltip: str
110 changes: 62 additions & 48 deletions magicgui/widgets/_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,10 @@ class Widget:
By default, ``name`` will be used. Note: ``name`` refers the name of the
parameter, as might be used in a signature, whereas label is just the label
for that widget in the GUI.
Whether the widget should be considered "only for the gui", or if it should be
included in any widget container signatures, by default False
tooltip : str, optional
A tooltip to display when hovering over the widget.
visible : bool, optional
Whether the widget is visible, by default ``True``.
backend_kwargs : dict, optional
keyword argument to pass to the backend widget constructor.
"""
Expand All @@ -201,7 +203,8 @@ def __init__(
widget_type: Type[_protocols.WidgetProtocol],
name: str = "",
annotation: Any = None,
label=None,
label: str = None,
tooltip: Optional[str] = None,
visible: bool = True,
gui_only=False,
backend_kwargs=dict(),
Expand All @@ -228,6 +231,7 @@ def __init__(
self.name: str = name
self.param_kind = inspect.Parameter.POSITIONAL_OR_KEYWORD
self._label = label
self.tooltip = tooltip
self.annotation: Any = annotation
self.gui_only = gui_only
self.visible: bool = True
Expand All @@ -241,9 +245,6 @@ def __init__(
if not visible:
self.hide()

def _emit_parent(self, event=None):
self.parent_changed(value=self.parent)

@property
def annotation(self):
"""Return type annotation for the parameter represented by the widget.
Expand Down Expand Up @@ -293,28 +294,6 @@ def native(self):
"""Return native backend widget."""
return self._widget._mgui_get_native_widget()

def show(self, run=False):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most of this is just moved in this PR... only new things is the tooltip property

"""Show the widget."""
self._widget._mgui_show_widget()
self.visible = True
if run:
self.__magicgui_app__.run()
return self # useful for generating repr in sphinx

@contextmanager
def shown(self):
"""Context manager to show the widget."""
try:
self.show()
yield self.__magicgui_app__.__enter__()
finally:
self.__magicgui_app__.__exit__()

def hide(self):
"""Hide widget."""
self._widget._mgui_hide_widget()
self.visible = False

@property
def enabled(self) -> bool:
"""Whether widget is enabled (editable)."""
Expand All @@ -338,10 +317,6 @@ def widget_type(self) -> str:
"""Return type of widget."""
return self.__class__.__name__

def __repr__(self) -> str:
"""Return representation of widget of instsance."""
return f"{self.widget_type}(annotation={self.annotation!r}, name={self.name!r})"

@property
def label(self):
"""Return a label to use for this widget when present in Containers."""
Expand All @@ -354,10 +329,63 @@ def label(self, value):
self._label = value
self.label_changed(value=value)

@property
def width(self) -> int:
"""Return the current width of the widget.

The naming of this method may change. The intention is to get the width of the
widget after it is shown, for the purpose of unifying widget width in a layout.
Backends may do what they need to accomplish this. For example, Qt can use
``sizeHint().width()``, since ``width()`` will return something large if the
widget has not yet been painted on screen.
"""
return self._widget._mgui_get_width()

@width.setter
def width(self, value: int) -> None:
"""Set the minimum allowable width of the widget."""
self._widget._mgui_set_min_width(value)

@property
def tooltip(self) -> Optional[str]:
"""Get the tooltip for this widget."""
return self._widget._mgui_get_tooltip() or None

@tooltip.setter
def tooltip(self, value: Optional[str]) -> None:
"""Set the tooltip for this widget."""
return self._widget._mgui_set_tooltip(value)

def show(self, run=False):
"""Show the widget."""
self._widget._mgui_show_widget()
self.visible = True
if run:
self.__magicgui_app__.run()
return self # useful for generating repr in sphinx

@contextmanager
def shown(self):
"""Context manager to show the widget."""
try:
self.show()
yield self.__magicgui_app__.__enter__()
finally:
self.__magicgui_app__.__exit__()

def hide(self):
"""Hide widget."""
self._widget._mgui_hide_widget()
self.visible = False

def render(self) -> "np.ndarray":
"""Return an RGBA (MxNx4) numpy array bitmap of the rendered widget."""
return self._widget._mgui_render()

def __repr__(self) -> str:
"""Return representation of widget of instsance."""
return f"{self.widget_type}(annotation={self.annotation!r}, name={self.name!r})"

def _repr_png_(self):
"""Return PNG representation of the widget for QtConsole."""
from io import BytesIO
Expand All @@ -376,22 +404,8 @@ def _repr_png_(self):
file_obj.seek(0)
return file_obj.read()

@property
def width(self) -> int:
"""Return the current width of the widget.

The naming of this method may change. The intention is to get the width of the
widget after it is shown, for the purpose of unifying widget width in a layout.
Backends may do what they need to accomplish this. For example, Qt can use
``sizeHint().width()``, since ``width()`` will return something large if the
widget has not yet been painted on screen.
"""
return self._widget._mgui_get_width()

@width.setter
def width(self, value: int) -> None:
"""Set the minimum allowable width of the widget."""
self._widget._mgui_set_min_width(value)
def _emit_parent(self, event=None):
self.parent_changed(value=self.parent)


UNBOUND = object()
Expand Down
Loading