-
Notifications
You must be signed in to change notification settings - Fork 116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Tidy] Convert actions to classes #363
base: dev/actions_v2
Are you sure you want to change the base?
Changes from 10 commits
6cbf33d
df0c7cf
8fac8e5
9851cb9
87db5d7
7b8a6c3
c40ea85
99b684d
0c260e6
76fe66e
6d88c50
400ac60
b823e6b
31bd5a3
0c65e2c
d0eac24
1107486
caf5012
6b15541
4d2ba9a
f1235c4
c8a2618
de20b21
80238ed
bedf430
3ed5f86
387ce00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import importlib | ||
from typing import Any, Dict, List, Literal | ||
|
||
from dash import Output, State, ctx, dcc | ||
|
||
from vizro.actions._filter_action import _filter | ||
from vizro.actions.filter_interaction_action import filter_interaction | ||
from vizro.managers import model_manager | ||
from vizro.models.types import CapturedActionCallable | ||
|
||
|
||
class ExportDataClassAction(CapturedActionCallable): | ||
def __init__(self, *args, **kwargs): | ||
self._args = args | ||
self._kwargs = kwargs | ||
# Fake initialization - to let other actions see that this one exists. | ||
super().__init__(*args, **kwargs) | ||
|
||
def _post_init(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be called directly in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's too early to call |
||
"""Post initialization is called in the vm.Action build phase, and it is used to validate and calculate the | ||
properties of the CapturedActionCallable. With this, we can validate the properties and raise errors before | ||
the action is built. Also, "input"/"output"/"components" properties and "pure_function" can use these validated | ||
and the calculated arguments. | ||
""" | ||
self._page_id = model_manager._get_model_page_id(model_id=self._action_id) | ||
|
||
# Validate and calculate "targets" | ||
targets = self._kwargs.get("targets") | ||
if targets: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels like an improvement on the old function version because we now validate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, that's right! 😄 |
||
for target in targets: | ||
if target not in model_manager: | ||
raise ValueError(f"Component '{target}' does not exist on the page '{self._page_id}'.") | ||
else: | ||
targets = model_manager._get_page_model_ids_with_figure(page_id=self._page_id) | ||
self._kwargs["targets"] = self.targets = targets | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I understand what's happening with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now this line looks like this:
|
||
|
||
# Validate and calculate "file_format" | ||
file_format = self._kwargs.get("file_format", "csv") | ||
if file_format not in ["csv", "xlsx"]: | ||
raise ValueError(f'Unknown "file_format": {file_format}.' f' Known file formats: "csv", "xlsx".') | ||
if file_format == "xlsx": | ||
if importlib.util.find_spec("openpyxl") is None and importlib.util.find_spec("xlsxwriter") is None: | ||
raise ModuleNotFoundError("You must install either openpyxl or xlsxwriter to export to xlsx format.") | ||
self._kwargs["file_format"] = self.file_format = file_format | ||
|
||
# Post initialization - to enable pure_function to use calculated input arguments like "targets". | ||
super().__init__(*self._args, **self._kwargs) | ||
|
||
@staticmethod | ||
def pure_function(targets: List[str], file_format: Literal["csv", "xlsx"] = "csv", **inputs: Dict[str, Any]): | ||
from vizro.actions._actions_utils import _get_filtered_data | ||
|
||
data_frames = _get_filtered_data( | ||
targets=targets, | ||
ctds_filters=ctx.args_grouping["external"]["filters"], | ||
ctds_filter_interaction=ctx.args_grouping["external"]["filter_interaction"], | ||
) | ||
|
||
outputs = {} | ||
for target_id in targets: | ||
if file_format == "csv": | ||
writer = data_frames[target_id].to_csv | ||
elif file_format == "xlsx": | ||
writer = data_frames[target_id].to_excel | ||
|
||
outputs[f"download_dataframe_{target_id}"] = dcc.send_data_frame( | ||
writer=writer, filename=f"{target_id}.{file_format}", index=False | ||
) | ||
|
||
return outputs | ||
|
||
@property | ||
def inputs(self): | ||
from vizro.actions._callback_mapping._callback_mapping_utils import ( | ||
_get_inputs_of_figure_interactions, | ||
_get_inputs_of_filters, | ||
) | ||
|
||
page = model_manager[self._page_id] | ||
return { | ||
"filters": _get_inputs_of_filters(page=page, action_function=_filter.__wrapped__), | ||
"filter_interaction": _get_inputs_of_figure_interactions( | ||
page=page, action_function=filter_interaction.__wrapped__ | ||
), | ||
# TODO-actions: Propagate theme_selector only if it exists on the page (could be overwritten by the user) | ||
"theme_selector": State("theme_selector", "checked"), | ||
} | ||
|
||
@property | ||
def outputs(self) -> Dict[str, Output]: | ||
# TODO-actions: Take the "actions_info" into account once it's implemented. | ||
return { | ||
f"download_dataframe_{target}": Output( | ||
component_id={"type": "download_dataframe", "action_id": self._action_id, "target_id": target}, | ||
component_property="data", | ||
) | ||
for target in self.targets | ||
} | ||
|
||
@property | ||
def components(self): | ||
# TODO-actions: Take the "actions_info" into account once it's implemented. | ||
return [ | ||
dcc.Download(id={"type": "download_dataframe", "action_id": self._action_id, "target_id": target}) | ||
for target in self.targets | ||
] | ||
|
||
|
||
# Alias for ExportDataClassAction | ||
export_data_class_action = ExportDataClassAction |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
import importlib.util | ||
import logging | ||
from collections.abc import Collection, Mapping | ||
from pprint import pformat | ||
|
@@ -9,7 +8,7 @@ | |
try: | ||
from pydantic.v1 import Field, validator | ||
except ImportError: # pragma: no cov | ||
from pydantic import Field, validator | ||
from pydantic import Field | ||
|
||
import vizro.actions | ||
from vizro.managers._model_manager import ModelID | ||
|
@@ -49,18 +48,9 @@ class Action(VizroBaseModel): | |
# require, and make the code here look up the appropriate validation using the function as key | ||
# This could then also involve other validations currently only carried out at run-time in pre-defined actions, such | ||
# as e.g. checking if the correct arguments have been provided to the file_format in export_data. | ||
@validator("function") | ||
def validate_predefined_actions(cls, function): | ||
if function._function.__name__ == "export_data": | ||
file_format = function._arguments.get("file_format") | ||
if file_format not in [None, "csv", "xlsx"]: | ||
raise ValueError(f'Unknown "file_format": {file_format}.' f' Known file formats: "csv", "xlsx".') | ||
if file_format == "xlsx": | ||
if importlib.util.find_spec("openpyxl") is None and importlib.util.find_spec("xlsxwriter") is None: | ||
raise ModuleNotFoundError( | ||
"You must install either openpyxl or xlsxwriter to export to xlsx format." | ||
) | ||
return function | ||
# | ||
# @validator("function") | ||
# def validate_predefined_actions(cls, function): | ||
|
||
def _get_callback_mapping(self): | ||
"""Builds callback inputs and outputs for the Action model callback, and returns action required components. | ||
|
@@ -77,8 +67,14 @@ def _get_callback_mapping(self): | |
from vizro.actions._callback_mapping._get_action_callback_mapping import _get_action_callback_mapping | ||
|
||
callback_inputs: Union[List[State], Dict[str, State]] | ||
# TODO-actions: Refactor the following lines to: | ||
# `callback_inputs = self.function.inputs + [State(*input.split(".")) for input in self.inputs]` | ||
# After refactoring that's mentioned above, test overwriting of the predefined action. | ||
# (by adding a new inputs/outputs to the overwritten action and check if it's working as expected) | ||
if self.inputs: | ||
callback_inputs = [State(*input.split(".")) for input in self.inputs] | ||
elif hasattr(self.function, "inputs") and self.function.inputs: | ||
callback_inputs = self.function.inputs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we're going to have a few switches here while we have both the "old" function actions and the new class ones. Let's have a consistent way of doing this everywhere to make it clearer. I think just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You're right, and after all actions become implemented as CapturedActionCallable, then the following line will be removed: |
||
else: | ||
callback_inputs = _get_action_callback_mapping(action_id=ModelID(str(self.id)), argument="inputs") | ||
|
||
|
@@ -91,10 +87,15 @@ def _get_callback_mapping(self): | |
# single element list (e.g. ["text"]). | ||
if len(callback_outputs) == 1: | ||
callback_outputs = callback_outputs[0] | ||
elif hasattr(self.function, "outputs") and self.function.outputs: | ||
callback_outputs = self.function.outputs | ||
else: | ||
callback_outputs = _get_action_callback_mapping(action_id=ModelID(str(self.id)), argument="outputs") | ||
|
||
action_components = _get_action_callback_mapping(action_id=ModelID(str(self.id)), argument="components") | ||
if hasattr(self.function, "components") and self.function.components: | ||
action_components = self.function.components | ||
else: | ||
action_components = _get_action_callback_mapping(action_id=ModelID(str(self.id)), argument="components") | ||
|
||
return callback_inputs, callback_outputs, action_components | ||
|
||
|
@@ -152,6 +153,11 @@ def build(self): | |
List of required components (e.g. dcc.Download) for the Action model added to the `Dashboard` container. | ||
|
||
""" | ||
# Consider sending the entire action object | ||
self.function._action_id = self.id | ||
if hasattr(self.function, "_post_init"): | ||
self.function._post_init() | ||
|
||
external_callback_inputs, external_callback_outputs, action_components = self._get_callback_mapping() | ||
callback_inputs = { | ||
"external": external_callback_inputs, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this
__init__
for?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used it to save input args and kwargs so they can be validated/adjusted in the
_post_init
.Now, I got rid of the constructor and
self._arguments
is used inside the_post_init
.