From 66cbb39af257fdb0087171630fc3259988057c34 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 18:03:16 -0400 Subject: [PATCH 1/5] style: use Unpack for kwargs --- pyproject.toml | 4 +-- src/magicgui/signature.py | 14 ++++++---- src/magicgui/type_map/_magicgui.py | 12 ++++++--- src/magicgui/widgets/_table.py | 9 ++++--- src/magicgui/widgets/bases/_button_widget.py | 8 ++++-- .../widgets/bases/_categorical_widget.py | 6 ++++- .../widgets/bases/_container_widget.py | 26 +++++++++++++++---- src/magicgui/widgets/bases/_ranged_widget.py | 25 ++++++------------ src/magicgui/widgets/bases/_slider_widget.py | 8 ++++-- src/magicgui/widgets/bases/_value_widget.py | 6 ++++- src/magicgui/widgets/bases/_widget.py | 20 +++++++++++++- 11 files changed, 95 insertions(+), 43 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3a292e2f..a99286a0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ keywords = ["gui", "widgets", "type annotations"] readme = "README.md" requires-python = ">=3.8" license = { text = "MIT" } -authors = [{ email = "talley.lambert@gmail.com" }, { name = "Talley Lambert" }] +authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }] classifiers = [ "Development Status :: 4 - Beta", "Environment :: X11 Applications :: Qt", @@ -31,7 +31,6 @@ classifiers = [ "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets", "Topic :: Utilities", - ] dynamic = ["version"] dependencies = [ @@ -230,6 +229,7 @@ disallow_any_generics = false disallow_subclassing_any = false show_error_codes = true pretty = true +enable_incomplete_feature = ['Unpack'] [[tool.mypy.overrides]] module = [ diff --git a/src/magicgui/signature.py b/src/magicgui/signature.py index d2db0115d..3b61c9a25 100644 --- a/src/magicgui/signature.py +++ b/src/magicgui/signature.py @@ -22,8 +22,12 @@ from magicgui.types import Undefined if TYPE_CHECKING: + from typing_extensions import Unpack + from magicgui.application import AppRef from magicgui.widgets import Container, Widget + from magicgui.widgets.bases._container_widget import ContainerKwargs + TZ_EMPTY = "__no__default__" @@ -229,14 +233,14 @@ def widgets(self, app: AppRef | None = None) -> MappingProxyType: {n: p.to_widget(app) for n, p in self.parameters.items()} ) - def to_container(self, **kwargs: Any) -> Container: + def to_container( + self, app: AppRef | None = None, **kwargs: Unpack[ContainerKwargs] + ) -> Container: """Return a ``magicgui.widgets.Container`` for this MagicSignature.""" from magicgui.widgets import Container - return Container( - widgets=list(self.widgets(kwargs.get("app")).values()), - **kwargs, - ) + kwargs["widgets"] = list(self.widgets(app).values()) + return Container(**kwargs) def replace( # type: ignore[override] self, diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py index c4a8d81a6..27155a002 100644 --- a/src/magicgui/type_map/_magicgui.py +++ b/src/magicgui/type_map/_magicgui.py @@ -413,6 +413,9 @@ def magic_factory( ) +MAGICGUI_PARAMS = inspect.signature(magicgui).parameters + + # _R is the return type of the decorated function # _T is the type of the FunctionGui instance (FunctionGui or MainFunctionGui) class MagicFactory(partial, Generic[_R, _T]): @@ -467,11 +470,10 @@ def __new__( def __repr__(self) -> str: """Return string repr.""" - params = inspect.signature(magicgui).parameters args = [ f"{k}={v!r}" for (k, v) in self.keywords.items() - if v not in (params[k].default, {}) + if v not in (MAGICGUI_PARAMS[k].default, {}) ] return f"MagicFactory({', '.join(args)})" @@ -479,10 +481,12 @@ def __call__(self, *args: Any, **kwargs: Any) -> _T: """Call the wrapped _magicgui and return a FunctionGui.""" if args: raise ValueError("MagicFactory instance only accept keyword arguments") - params = inspect.signature(magicgui).parameters + factory_kwargs = self.keywords.copy() prm_options = factory_kwargs.pop("param_options", {}) - prm_options.update({k: kwargs.pop(k) for k in list(kwargs) if k not in params}) + prm_options.update( + {k: kwargs.pop(k) for k in list(kwargs) if k not in MAGICGUI_PARAMS} + ) widget = self.func(param_options=prm_options, **{**factory_kwargs, **kwargs}) if self._widget_init is not None: self._widget_init(widget) diff --git a/src/magicgui/widgets/_table.py b/src/magicgui/widgets/_table.py index e347653f1..c307a9e5c 100644 --- a/src/magicgui/widgets/_table.py +++ b/src/magicgui/widgets/_table.py @@ -31,9 +31,12 @@ if TYPE_CHECKING: import numpy import pandas - from typing_extensions import TypeGuard + from typing_extensions import TypeGuard, Unpack from magicgui.widgets.protocols import TableWidgetProtocol + + from .bases._widget import WidgetKwargs + TblKey = Any _KT = TypeVar("_KT") # Key type _KT_co = TypeVar("_KT_co", covariant=True) # Key type covariant containers. @@ -227,9 +230,9 @@ def __init__( *, index: Collection | None = None, columns: Collection | None = None, - **kwargs: Any, + **kwargs: Unpack[WidgetKwargs], ) -> None: - super().__init__(widget_type=use_app().get_obj("Table"), **kwargs) + super().__init__(**{**kwargs, "widget_type": use_app().get_obj("Table")}) self._data = DataView(self) data, _index, _columns = normalize_table_data(value) self.value = { diff --git a/src/magicgui/widgets/bases/_button_widget.py b/src/magicgui/widgets/bases/_button_widget.py index 52c2b2e2b..7deba402b 100644 --- a/src/magicgui/widgets/bases/_button_widget.py +++ b/src/magicgui/widgets/bases/_button_widget.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Callable from psygnal import Signal, SignalInstance @@ -9,8 +9,12 @@ from ._value_widget import ValueWidget if TYPE_CHECKING: + from typing_extensions import Unpack + from magicgui.widgets import protocols + from ._widget import WidgetKwargs + class ButtonWidget(ValueWidget[bool]): """Widget with a value, Wraps a widget implementing the ButtonWidgetProtocol. @@ -50,7 +54,7 @@ def __init__( text: str | None = None, bind: bool | Callable[[ValueWidget], bool] | _Undefined = Undefined, nullable: bool = False, - **base_widget_kwargs: Any, + **base_widget_kwargs: Unpack[WidgetKwargs], ) -> None: if text and base_widget_kwargs.get("label"): from warnings import warn diff --git a/src/magicgui/widgets/bases/_categorical_widget.py b/src/magicgui/widgets/bases/_categorical_widget.py index 1eaf04eb8..a91eff746 100644 --- a/src/magicgui/widgets/bases/_categorical_widget.py +++ b/src/magicgui/widgets/bases/_categorical_widget.py @@ -8,8 +8,12 @@ from ._value_widget import T, ValueWidget if TYPE_CHECKING: + from typing_extensions import Unpack + from magicgui.widgets import protocols + from ._widget import WidgetKwargs + class CategoricalWidget(ValueWidget[T]): """Widget with a value and choices. Wraps CategoricalWidgetProtocol. @@ -45,7 +49,7 @@ def __init__( allow_multiple: bool | None = None, bind: T | Callable[[ValueWidget], T] | _Undefined = Undefined, nullable: bool = False, - **base_widget_kwargs: Any, + **base_widget_kwargs: Unpack[WidgetKwargs], ) -> None: if allow_multiple is not None: self._allow_multiple = allow_multiple diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index e595548aa..da14ec091 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -29,7 +29,19 @@ import inspect from pathlib import Path + from typing_extensions import Unpack + from magicgui.widgets import Container, protocols + + from ._widget import WidgetKwargs + + class ContainerKwargs(WidgetKwargs, total=False): + widgets: Sequence[Widget] + layout: str + scrollable: bool + labels: bool + + WidgetVar = TypeVar("WidgetVar", bound=Widget) @@ -94,14 +106,13 @@ def __init__( layout: str = "vertical", scrollable: bool = False, labels: bool = True, - **base_widget_kwargs: Any, + **base_widget_kwargs: Unpack[WidgetKwargs], ) -> None: self._list: list[WidgetVar] = [] self._labels = labels self._layout = layout self._scrollable = scrollable - base_widget_kwargs.setdefault("backend_kwargs", {}) - base_widget_kwargs["backend_kwargs"].update( + base_widget_kwargs.setdefault("backend_kwargs", {}).update( # type: ignore {"layout": layout, "scrollable": scrollable} ) super().__init__(**base_widget_kwargs) @@ -284,13 +295,18 @@ def __signature__(self) -> MagicSignature: return MagicSignature(params) @classmethod - def from_signature(cls, sig: inspect.Signature, **kwargs: Any) -> Container: + def from_signature( + cls, sig: inspect.Signature, **kwargs: Unpack[ContainerKwargs] + ) -> Container: """Create a Container widget from an inspect.Signature object.""" return MagicSignature.from_signature(sig).to_container(**kwargs) @classmethod def from_callable( - cls, obj: Callable, gui_options: dict | None = None, **kwargs: Any + cls, + obj: Callable, + gui_options: dict | None = None, + **kwargs: Unpack[ContainerKwargs], ) -> Container: """Create a Container widget from a callable object. diff --git a/src/magicgui/widgets/bases/_ranged_widget.py b/src/magicgui/widgets/bases/_ranged_widget.py index 685bf2877..b6a0e0933 100644 --- a/src/magicgui/widgets/bases/_ranged_widget.py +++ b/src/magicgui/widgets/bases/_ranged_widget.py @@ -3,16 +3,19 @@ import builtins from abc import ABC, abstractmethod from math import ceil, log10 -from typing import TYPE_CHECKING, Any, Callable, Iterable, Tuple, TypeVar, Union, cast -from warnings import warn +from typing import TYPE_CHECKING, Callable, Iterable, Tuple, TypeVar, Union, cast from magicgui.types import Undefined, _Undefined from ._value_widget import ValueWidget if TYPE_CHECKING: + from typing_extensions import Unpack + from magicgui.widgets import protocols + from ._widget import WidgetKwargs + T = TypeVar("T", int, float, Tuple[Union[int, float], ...]) DEFAULT_MIN = 0.0 DEFAULT_MAX = 1000.0 @@ -56,20 +59,8 @@ def __init__( step: float | _Undefined | None = Undefined, bind: T | Callable[[ValueWidget], T] | _Undefined = Undefined, nullable: bool = False, - **base_widget_kwargs: Any, - ) -> None: # sourcery skip: avoid-builtin-shadow - for key in ("maximum", "minimum"): - if key in base_widget_kwargs: - warn( - f"The {key!r} keyword arguments has been changed to {key[:3]!r}. " - "In the future this will raise an exception\n", - FutureWarning, - stacklevel=2, - ) - if key == "maximum": - max = base_widget_kwargs.pop(key) # noqa: A001 - else: - min = base_widget_kwargs.pop(key) # noqa: A001 + **base_widget_kwargs: Unpack[WidgetKwargs], + ) -> None: # value should be set *after* min max is set super().__init__( bind=bind, # type: ignore @@ -242,7 +233,7 @@ def __init__( step: int = 1, bind: T | Callable[[ValueWidget], T] | _Undefined = Undefined, nullable: bool = False, - **base_widget_kwargs: Any, + **base_widget_kwargs: Unpack[WidgetKwargs], ) -> None: self._min = min self._max = max diff --git a/src/magicgui/widgets/bases/_slider_widget.py b/src/magicgui/widgets/bases/_slider_widget.py index bcf732a49..2198c33ee 100644 --- a/src/magicgui/widgets/bases/_slider_widget.py +++ b/src/magicgui/widgets/bases/_slider_widget.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Tuple, Union +from typing import TYPE_CHECKING, Callable, Tuple, Union from magicgui.types import Undefined, _Undefined @@ -8,8 +8,12 @@ from ._ranged_widget import MultiValueRangedWidget, RangedWidget, T if TYPE_CHECKING: + from typing_extensions import Unpack + from magicgui.widgets import protocols + from ._widget import WidgetKwargs + class SliderWidget(RangedWidget[T], _OrientationMixin): """Widget with a constrained value and orientation. Wraps SliderWidgetProtocol. @@ -61,7 +65,7 @@ def __init__( tracking: bool = True, bind: T | Callable[[protocols.ValueWidgetProtocol], T] | _Undefined = Undefined, nullable: bool = False, - **base_widget_kwargs: Any, + **base_widget_kwargs: Unpack[WidgetKwargs], ) -> None: base_widget_kwargs["backend_kwargs"] = { "readout": readout, diff --git a/src/magicgui/widgets/bases/_value_widget.py b/src/magicgui/widgets/bases/_value_widget.py index 2ca345913..5ce3c0545 100644 --- a/src/magicgui/widgets/bases/_value_widget.py +++ b/src/magicgui/widgets/bases/_value_widget.py @@ -10,8 +10,12 @@ from ._widget import Widget if TYPE_CHECKING: + from typing_extensions import Unpack + from magicgui.widgets import protocols + from ._widget import WidgetKwargs + T = TypeVar("T") @@ -45,7 +49,7 @@ def __init__( *, bind: T | Callable[[ValueWidget], T] | _Undefined = Undefined, nullable: bool = False, - **base_widget_kwargs: Any, + **base_widget_kwargs: Unpack[WidgetKwargs], ) -> None: self._nullable = nullable self._bound_value = bind diff --git a/src/magicgui/widgets/bases/_widget.py b/src/magicgui/widgets/bases/_widget.py index 22540515d..791e7d7ad 100644 --- a/src/magicgui/widgets/bases/_widget.py +++ b/src/magicgui/widgets/bases/_widget.py @@ -15,10 +15,28 @@ from weakref import ReferenceType import numpy as np + from typing_extensions import TypedDict from magicgui.widgets._concrete import _LabeledWidget from magicgui.widgets.protocols import WidgetProtocol + class WidgetKwargs(TypedDict, total=False): + # technically, this should be Required[type[WidgetProtocol]]] + # but the widget_type argument is generally provided dynamically + # by the @backend_widget decorator. So it appears to be missing if we require it + widget_type: type[WidgetProtocol] + name: str + annotation: Any | None + label: str | None + tooltip: str | None + visible: bool | None + enabled: bool + gui_only: bool + parent: Any + backend_kwargs: dict | None + + description: str # alias for label + class Widget: """Basic Widget, wrapping a class that implements WidgetProtocol. @@ -77,7 +95,7 @@ def __init__( gui_only: bool = False, parent: Any | None = None, backend_kwargs: dict | None = None, - **extra: Any, + **extra: Any, # not really used ): # for ipywidgets API compatibility if backend_kwargs is None: From 86b259ce3bf7888a1f6471c1feedb77a2bccb8a7 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 18:06:22 -0400 Subject: [PATCH 2/5] remove another deprecation --- src/magicgui/widgets/_concrete.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py index 820096f3f..187347fc1 100644 --- a/src/magicgui/widgets/_concrete.py +++ b/src/magicgui/widgets/_concrete.py @@ -285,21 +285,6 @@ def __init__( tracking: bool = True, **kwargs: Any, ): - # sourcery skip: avoid-builtin-shadow - for key in ("maximum", "minimum"): - if key in kwargs: - import warnings - - warnings.warn( - f"The {key!r} keyword arguments has been changed to {key[:3]!r}. " - "In the future this will raise an exception\n", - FutureWarning, - stacklevel=2, - ) - if key == "maximum": - max = kwargs.pop(key) # noqa: A001 - else: - min = kwargs.pop(key) # noqa: A001 self._base = base app = use_app() assert app.native From a2edde2c134f41a7ff4ee47e9752c0a7f63a5054 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 18:23:30 -0400 Subject: [PATCH 3/5] more kwargs --- src/magicgui/widgets/_concrete.py | 45 ++++++++++--------- .../widgets/bases/_container_widget.py | 10 ++--- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py index 187347fc1..f7ea08ba3 100644 --- a/src/magicgui/widgets/_concrete.py +++ b/src/magicgui/widgets/_concrete.py @@ -54,7 +54,12 @@ from magicgui.widgets.bases._mixins import _OrientationMixin, _ReadOnlyMixin if TYPE_CHECKING: + from typing_extensions import Unpack + from magicgui.widgets import protocols + from magicgui.widgets.bases._container_widget import ContainerKwargs + from magicgui.widgets.bases._widget import WidgetKwargs + WidgetVar = TypeVar("WidgetVar", bound=Widget) WidgetTypeVar = TypeVar("WidgetTypeVar", bound=Type[Widget]) @@ -283,17 +288,13 @@ def __init__( max: float = 100, base: float = math.e, tracking: bool = True, - **kwargs: Any, + **kwargs: Unpack[WidgetKwargs], ): self._base = base app = use_app() assert app.native - super().__init__( - min=min, - max=max, - widget_type=app.get_obj("Slider"), - **kwargs, - ) + kwargs["widget_type"] = app.get_obj("Slider") + super().__init__(min=min, max=max, **kwargs) self.tracking = tracking @property @@ -356,7 +357,10 @@ class RadioButtons(CategoricalWidget, _OrientationMixin): # type: ignore """An exclusive group of radio buttons, providing a choice from multiple choices.""" def __init__( - self, choices: ChoicesType = (), orientation: str = "vertical", **kwargs: Any + self, + choices: ChoicesType = (), + orientation: str = "vertical", + **kwargs: Unpack[WidgetKwargs], ) -> None: app = use_app() assert app.native @@ -407,9 +411,10 @@ def __init__( mode: FileDialogMode = FileDialogMode.EXISTING_FILE, filter: str | None = None, nullable: bool = False, - **kwargs: Any, + **kwargs: Unpack[ContainerKwargs], ) -> None: - value = kwargs.pop("value", None) + # use empty string as a null value + value = kwargs.pop("value", None) # type: ignore [typeddict-item] if value is None: value = "" self.line_edit = LineEdit(value=value) @@ -518,9 +523,9 @@ def __init__( step: int = 1, min: int | tuple[int, int, int] | None = None, max: int | tuple[int, int, int] | None = None, - **kwargs: Any, + **kwargs: Unpack[ContainerKwargs], ) -> None: - value = kwargs.pop("value", None) + value = kwargs.pop("value", None) # type: ignore [typeddict-item] if value is not None and value is not Undefined: if not all(hasattr(value, x) for x in ("start", "stop", "step")): raise TypeError(f"Invalid value type for {type(self)}: {type(value)}") @@ -533,7 +538,7 @@ def __init__( kwargs["widgets"] = [self.start, self.stop, self.step] kwargs.setdefault("layout", "horizontal") kwargs.setdefault("labels", True) - kwargs.pop("nullable", None) + kwargs.pop("nullable", None) # type: ignore [typeddict-item] super().__init__(**kwargs) @classmethod @@ -616,13 +621,13 @@ class ListEdit(Container[ValueWidget[_V]]): All additional keyword arguments are passed to `Container` constructor. """ - def __init__( + def __init__( # type: ignore [misc] # overlap between names self, value: Iterable[_V] | _Undefined = Undefined, layout: str = "horizontal", nullable: bool = False, options: dict | None = None, - **kwargs: Any, + **kwargs: Unpack[ContainerKwargs], ) -> None: self._args_type: type | None = None self._nullable = nullable @@ -873,14 +878,14 @@ def __init__( self, value: Iterable[_V] | _Undefined = Undefined, *, - layout: str = "horizontal", nullable: bool = False, options: dict | None = None, - **kwargs: Any, + **container_kwargs: Unpack[ContainerKwargs[ValueWidget]], ) -> None: self._nullable = nullable self._args_types: tuple[type, ...] | None = None - super().__init__(layout=layout, labels=False, **kwargs) + container_kwargs["labels"] = False + super().__init__(**container_kwargs) self._child_options = options or {} self.margins = (0, 0, 0, 0) @@ -967,12 +972,12 @@ def value(self, vals: Sequence) -> None: class _LabeledWidget(Container): """Simple container that wraps a widget and provides a label.""" - def __init__( + def __init__( # type: ignore [misc] # overlap between argument names self, widget: Widget, label: str | None = None, position: str = "left", - **kwargs: Any, + **kwargs: Unpack[ContainerKwargs], ) -> None: kwargs["layout"] = "horizontal" if position in {"left", "right"} else "vertical" self._inner_widget = widget diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index da14ec091..a1fc17ee9 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -5,6 +5,7 @@ TYPE_CHECKING, Any, Callable, + Generic, Iterable, Mapping, MutableSequence, @@ -25,6 +26,8 @@ from ._value_widget import ValueWidget from ._widget import Widget +WidgetVar = TypeVar("WidgetVar", bound=Widget) + if TYPE_CHECKING: import inspect from pathlib import Path @@ -35,16 +38,13 @@ from ._widget import WidgetKwargs - class ContainerKwargs(WidgetKwargs, total=False): - widgets: Sequence[Widget] + class ContainerKwargs(WidgetKwargs, Generic[WidgetVar], total=False): + widgets: Sequence[WidgetVar] layout: str scrollable: bool labels: bool -WidgetVar = TypeVar("WidgetVar", bound=Widget) - - class ContainerWidget(Widget, _OrientationMixin, MutableSequence[WidgetVar]): """Widget that can contain other widgets. From 85c4e908f3e06ae88a9103de7621117666ff7ffe Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 18:24:12 -0400 Subject: [PATCH 4/5] fix napari test --- .github/workflows/test_and_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 86a25a443..f5fa85af2 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -98,7 +98,7 @@ jobs: python-version: "3.10" - name: Install run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip pytest-pretty python -m pip install -e .[testing] python -m pip install -e ./napari-from-github[pyqt5] From e7e0cef1840a34cc030b0ffd5a530f32d593e731 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 18:26:09 -0400 Subject: [PATCH 5/5] cleanup table --- src/magicgui/widgets/_table.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/magicgui/widgets/_table.py b/src/magicgui/widgets/_table.py index c307a9e5c..0dc833d89 100644 --- a/src/magicgui/widgets/_table.py +++ b/src/magicgui/widgets/_table.py @@ -232,7 +232,8 @@ def __init__( columns: Collection | None = None, **kwargs: Unpack[WidgetKwargs], ) -> None: - super().__init__(**{**kwargs, "widget_type": use_app().get_obj("Table")}) + kwargs["widget_type"] = use_app().get_obj("Table") + super().__init__(**kwargs) self._data = DataView(self) data, _index, _columns = normalize_table_data(value) self.value = {