diff --git a/docs/conf.py b/docs/conf.py index f514a49..580e40c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,14 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import nltk + +# Make sure stopwords are available for autodoc +try: + nltk.data.find("corpora/stopwords") +except LookupError: + nltk.download("stopwords") + # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, @@ -27,7 +35,6 @@ # -- General configuration --------------------------------------------------- - # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. @@ -40,6 +47,7 @@ "sphinx_new_tab_link", "myst_nb", "sphinx_copybutton", + "sphinxcontrib.autodoc_pydantic", ] # https://myst-nb.readthedocs.io/en/latest/computation/execute.html @@ -81,7 +89,6 @@ "conf.py", ] - # Intersphinx options intersphinx_mapping = { "python": ("https://docs.python.org/3", None), @@ -93,6 +100,30 @@ "numpy": ("https://numpy.org/doc/stable/", None), } +# Options for the autodoc_pydantic extension +autodoc_pydantic_field_show_default = True +autodoc_pydantic_field_show_constraints = True +autodoc_pydantic_field_list_validators = True +autodoc_pydantic_model_signature_prefix = "class" +autodoc_pydantic_field_signature_prefix = "attribute" +autodoc_pydantic_field_show_required = True +autodoc_pydantic_field_show_optional = True +autodoc_pydantic_field_show_default = True +autodoc_pydantic_field_doc_policy = "description" +autodoc_pydantic_model_member_order = "bysource" +autodoc_pydantic_model_show_json = False +autodoc_pydantic_model_show_field_summary = False +autodoc_pydantic_model_show_config_summary = False +autodoc_pydantic_model_show_validator_summary = False +autodoc_pydantic_model_hide_paramlist = False +autodoc_pydantic_field_list_validators = False + +# Options for the autodoc extension +autodoc_default_options = { + "inherited-members": "BaseModel", + "show-inheritance": True, +} + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/pyproject.toml b/pyproject.toml index ea34ca0..3eba50f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ docs = [ "sphinx-new-tab-link!=0.2.2", "jupytext", "sphinx-copybutton", + "autodoc_pydantic", ] dev = [ "black", diff --git a/src/vuecore/plots/__init__.py b/src/vuecore/plots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/vuecore/plots/basic/bar.py b/src/vuecore/plots/basic/bar.py index 1295cca..7f25d2d 100644 --- a/src/vuecore/plots/basic/bar.py +++ b/src/vuecore/plots/basic/bar.py @@ -5,8 +5,10 @@ from vuecore import EngineType from vuecore.engines import get_builder, get_saver from vuecore.schemas.basic.bar import BarConfig +from vuecore.utils.docs_utils import document_pydant_params +@document_pydant_params(BarConfig) def create_bar_plot( data: pd.DataFrame, engine: EngineType = EngineType.PLOTLY, @@ -34,11 +36,6 @@ def create_bar_plot( The file format is automatically inferred from the file extension (e.g., '.html', '.png', '.jpeg', '.svg'). Defaults to None, meaning the plot will not be saved. - **kwargs - Keyword arguments for plot configuration. These arguments are - validated against the `BarConfig` Pydantic model. Refer to - `vuecore.schemas.basic.bar.BarConfig` for all available - options and their descriptions. Returns ------- diff --git a/src/vuecore/plots/basic/box.py b/src/vuecore/plots/basic/box.py index f2e0359..6f3432b 100644 --- a/src/vuecore/plots/basic/box.py +++ b/src/vuecore/plots/basic/box.py @@ -5,8 +5,10 @@ from vuecore import EngineType from vuecore.engines import get_builder, get_saver from vuecore.schemas.basic.box import BoxConfig +from vuecore.utils.docs_utils import document_pydant_params +@document_pydant_params(BoxConfig) def create_box_plot( data: pd.DataFrame, engine: EngineType = EngineType.PLOTLY, @@ -34,11 +36,6 @@ def create_box_plot( The file format is automatically inferred from the file extension (e.g., '.html', '.png', '.jpeg', '.svg'). Defaults to None, meaning the plot will not be saved. - **kwargs - Keyword arguments for plot configuration. These arguments are - validated against the `BoxConfig` Pydantic model. Refer to - `vuecore.schemas.basic.box.BoxConfig` for all available - options and their descriptions. Returns ------- diff --git a/src/vuecore/plots/basic/line.py b/src/vuecore/plots/basic/line.py index 0324ce2..1a87b46 100644 --- a/src/vuecore/plots/basic/line.py +++ b/src/vuecore/plots/basic/line.py @@ -5,8 +5,10 @@ from vuecore import EngineType from vuecore.engines import get_builder, get_saver from vuecore.schemas.basic.line import LineConfig +from vuecore.utils.docs_utils import document_pydant_params +@document_pydant_params(LineConfig) def create_line_plot( data: pd.DataFrame, engine: EngineType = EngineType.PLOTLY, @@ -34,10 +36,6 @@ def create_line_plot( The file format is automatically inferred from the file extension (e.g., '.html', '.png', '.jpeg', '.svg'). Defaults to None, meaning the plot will not be saved. - **kwargs - Keyword arguments for plot configuration. These arguments are validated - against the `LineConfig` Pydantic model. See - `vuecore.schemas.basic.line.LineConfig` for all available options. Returns ------- diff --git a/src/vuecore/plots/basic/scatter.py b/src/vuecore/plots/basic/scatter.py index 2a05cab..58e846d 100644 --- a/src/vuecore/plots/basic/scatter.py +++ b/src/vuecore/plots/basic/scatter.py @@ -5,8 +5,10 @@ from vuecore import EngineType from vuecore.engines import get_builder, get_saver from vuecore.schemas.basic.scatter import ScatterConfig +from vuecore.utils.docs_utils import document_pydant_params +@document_pydant_params(ScatterConfig) def create_scatter_plot( data: pd.DataFrame, engine: EngineType = EngineType.PLOTLY, @@ -34,10 +36,6 @@ def create_scatter_plot( The file format is automatically inferred from the file extension (e.g., '.html', '.png', '.jpeg', '.svg'). Defaults to None, meaning the plot will not be saved. - **kwargs - Keyword arguments for plot configuration. These arguments are validated - against the `ScatterConfig` Pydantic model. See - `schemas.basic.scatter.ScatterConfig` for all available options. Returns ------- diff --git a/src/vuecore/schemas/__init__.py b/src/vuecore/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/vuecore/schemas/basic/bar.py b/src/vuecore/schemas/basic/bar.py index 52f9b57..51a2e67 100644 --- a/src/vuecore/schemas/basic/bar.py +++ b/src/vuecore/schemas/basic/bar.py @@ -1,10 +1,12 @@ -from typing import Dict, List, Optional -from pydantic import BaseModel, Field, ConfigDict +from typing import Dict, Optional +from pydantic import Field, ConfigDict +from vuecore.schemas.plotly_base import PlotlyBaseConfig -class BarConfig(BaseModel): +class BarConfig(PlotlyBaseConfig): """ - Pydantic model for validating and managing bar plot configurations. + Pydantic model for validating and managing bar plot configurations, + which extends PlotlyBaseConfig. This model serves as a curated API for the most relevant parameters for bar plots, closely aligned with the `plotly.express.bar` API @@ -16,67 +18,6 @@ class BarConfig(BaseModel): defined here, and also accepts additional Plotly keyword arguments, forwarding them to the appropriate `plotly.express.bar` or `plotly.graph_objects.Figure` call. - - Attributes - ---------- - -----Data Mapping----- - x : str - Column for the x-axis values. - y : str - Column for the y-axis values. - color : Optional[str] - Column to assign color to bars. - pattern_shape : Optional[str] - Column to assign pattern shapes to bars. - hover_name : Optional[str] - Column to appear in bold in the hover tooltip. - hover_data : Optional[List[str]] - Additional columns to display in the hover tooltip. - text : Optional[str] - Column for adding text labels to bars. - facet_row : Optional[str] - Column to create vertical subplots (facets). - facet_col : Optional[str] - Column to create horizontal subplots (facets). - error_x : Optional[str] - Column for sizing x-axis error bars. - error_y : Optional[str] - Column for sizing y-axis error bars. - labels : Optional[Dict[str, str]] - Dictionary to override column names for titles, legends, etc. - color_discrete_map : Optional[Dict[str, str]] - Specific color mappings for values in the `color` column. - pattern_shape_map : Optional[Dict[str, str]] - Specific pattern shape mappings for values in the `pattern_shape` column. - -----Styling and Layout----- - opacity : float - Marker opacity (0-1). - orientation: str - Orientation of the bars ('v' for vertical, 'h' for horizontal). - barmode : str - Mode for grouping bars ('group', 'overlay', 'relative'). - log_x : bool - If True, the x-axis is log-scaled. - log_y : bool - If True, the y-axis is log-scaled. - range_x : Optional[List[float]] - Range for the x-axis, e.g., [0, 100]. - range_y : Optional[List[float]] - Range for the y-axis, e.g., [0, 100]. - title : str - The main title of the plot. - x_title : Optional[str] - Custom title for the x-axis. - y_title : Optional[str] - Custom title for the y-axis. - subtitle : str - The subtitle of the plot. - template : str - Plotly template for styling (e.g., 'plotly_white'). - width : int - Width of the plot in pixels. - height : int - Height of the plot in pixels. """ # General Configuration @@ -84,33 +25,12 @@ class BarConfig(BaseModel): model_config = ConfigDict(extra="allow") # Data Mapping - x: str = Field(..., description="Column for x-axis values.") - y: str = Field(..., description="Column for y-axis values.") - color: Optional[str] = Field(None, description="Column to assign color to bars.") pattern_shape: Optional[str] = Field( None, description="Column to assign pattern shapes to bars." ) - hover_name: Optional[str] = Field( - None, description="Column for bold text in hover tooltip." - ) - hover_data: List[str] = Field( - [], description="Additional columns for the hover tooltip." - ) text: Optional[str] = Field(None, description="Column for text labels on bars.") - facet_row: Optional[str] = Field( - None, description="Column to create vertical subplots." - ) - facet_col: Optional[str] = Field( - None, description="Column to create horizontal subplots." - ) error_x: Optional[str] = Field(None, description="Column for x-axis error bars.") error_y: Optional[str] = Field(None, description="Column for y-axis error bars.") - labels: Optional[Dict[str, str]] = Field( - None, description="Override column names in the plot." - ) - color_discrete_map: Optional[Dict[str, str]] = Field( - None, description="Map values to specific colors." - ) pattern_shape_map: Optional[Dict[str, str]] = Field( None, description="Map values to specific pattern shapes." ) @@ -122,18 +42,3 @@ class BarConfig(BaseModel): description="Orientation of the bars ('v' for vertical, 'h' for horizontal).", ) barmode: str = Field("relative", description="Mode for grouping bars.") - log_x: bool = Field(False, description="If True, use a logarithmic x-axis.") - log_y: bool = Field(False, description="If True, use a logarithmic y-axis.") - range_x: Optional[List[float]] = Field( - None, description="Range for the x-axis, e.g., [0, 100]." - ) - range_y: Optional[List[float]] = Field( - None, description="Range for the y-axis, e.g., [0, 100]." - ) - title: str = Field("Bar Plot", description="The main title of the plot.") - x_title: Optional[str] = Field(None, description="Custom title for the x-axis.") - y_title: Optional[str] = Field(None, description="Custom title for the y-axis.") - subtitle: Optional[str] = Field(None, description="The subtitle for the plot.") - template: str = Field("plotly_white", description="Plotly template for styling.") - width: int = Field(800, description="Width of the plot in pixels.") - height: int = Field(600, description="Height of the plot in pixels.") diff --git a/src/vuecore/schemas/basic/box.py b/src/vuecore/schemas/basic/box.py index 50f9d4e..c8ed9c1 100644 --- a/src/vuecore/schemas/basic/box.py +++ b/src/vuecore/schemas/basic/box.py @@ -1,10 +1,12 @@ -from typing import Dict, List, Optional -from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from pydantic import Field, ConfigDict +from vuecore.schemas.plotly_base import PlotlyBaseConfig -class BoxConfig(BaseModel): +class BoxConfig(PlotlyBaseConfig): """ - Pydantic model for validating and managing box plot configurations. + Pydantic model for validating and managing box plot configurations, + which extends PlotlyBaseConfig. This model serves as a curated API for the most relevant parameters for box plots, closely aligned with the `plotly.express.box` API @@ -15,116 +17,20 @@ class BoxConfig(BaseModel): structure. The plotting function handles parameters defined here, and also accepts additional Plotly keyword arguments, forwarding them to the appropriate `plotly.express.box` or `plotly.graph_objects.Figure` call. - - Attributes - ---------- - -----Data Mapping----- - x : Optional[str] - Column for the x-axis values. - y : Optional[str] - Column for the y-axis values. - color : Optional[str] - Column to assign color to box plots. - hover_name : Optional[str] - Column to appear in bold in the hover tooltip. - hover_data : List[str] - Additional columns to display in the hover tooltip. - facet_row : Optional[str] - Column to create vertical subplots (facets). - facet_col : Optional[str] - Column to create horizontal subplots (facets). - labels : Optional[Dict[str, str]] - Dictionary to override column names for titles, legends, etc. - color_discrete_map : Optional[Dict[str, str]] - Specific color mappings for values in the `color` column. - category_orders : Optional[Dict[str, List[str]]] - Dictionary to specify the order of categorical values. - -----Styling and Layout----- - orientation: str - Orientation of the box plots ('v' for vertical, 'h' for horizontal). - boxmode : str - Mode for grouping boxes ('group' or 'overlay'). - log_x : bool - If True, the x-axis is log-scaled. - log_y : bool - If True, the y-axis is log-scaled. - range_x : Optional[List[float]] - Range for the x-axis, e.g., [0, 100]. - range_y : Optional[List[float]] - Range for the y-axis, e.g., [0, 100]. - notched : bool - If True, boxes are drawn with notches. - points : str - Method to display sample points ('outliers', 'all', 'suspectedoutliers', False). - title : str - The main title of the plot. - x_title : Optional[str] - Custom title for the x-axis. - y_title : Optional[str] - Custom title for the y-axis. - subtitle : Optional[str] - The subtitle of the plot. - template : str - Plotly template for styling (e.g., 'plotly_white'). - width : int - Width of the plot in pixels. - height : int - Height of the plot in pixels. """ # General Configuration # Allow extra parameters to pass through to Plotly model_config = ConfigDict(extra="allow") - # Data Mapping - x: Optional[str] = Field(None, description="Column for x-axis values.") - y: Optional[str] = Field(None, description="Column for y-axis values.") - color: Optional[str] = Field(None, description="Column to assign color to boxes.") - hover_name: Optional[str] = Field( - None, description="Column for bold text in hover tooltip." - ) - hover_data: List[str] = Field( - [], description="Additional columns for the hover tooltip." - ) - facet_row: Optional[str] = Field( - None, description="Column to create vertical subplots." - ) - facet_col: Optional[str] = Field( - None, description="Column to create horizontal subplots." - ) - labels: Optional[Dict[str, str]] = Field( - None, description="Override column names in the plot." - ) - color_discrete_map: Optional[Dict[str, str]] = Field( - None, description="Map values to specific colors." - ) - category_orders: Optional[Dict[str, List[str]]] = Field( - None, description="Dictionary to specify the order of categorical values." - ) - # Styling and Layout orientation: Optional[str] = Field( None, description="Orientation of the box plots ('v' for vertical, 'h' for horizontal).", ) boxmode: str = Field("group", description="Mode for grouping boxes.") - log_x: bool = Field(False, description="If True, use a logarithmic x-axis.") - log_y: bool = Field(False, description="If True, use a logarithmic y-axis.") - range_x: Optional[List[float]] = Field( - None, description="Range for the x-axis, e.g., [0, 100]." - ) - range_y: Optional[List[float]] = Field( - None, description="Range for the y-axis, e.g., [0, 100]." - ) notched: bool = Field(False, description="If True, boxes are drawn with notches.") points: str = Field( "outliers", description="Method to display sample points ('outliers', 'all', 'suspectedoutliers', False).", ) - title: str = Field("Box Plot", description="The main title of the plot.") - x_title: Optional[str] = Field(None, description="Custom title for the x-axis.") - y_title: Optional[str] = Field(None, description="Custom title for the y-axis.") - subtitle: Optional[str] = Field(None, description="The subtitle of the plot.") - template: str = Field("plotly_white", description="Plotly template for styling.") - width: Optional[int] = Field(800, description="Width of the plot in pixels.") - height: Optional[int] = Field(600, description="Height of the plot in pixels.") diff --git a/src/vuecore/schemas/basic/line.py b/src/vuecore/schemas/basic/line.py index 02940f6..ff20b65 100644 --- a/src/vuecore/schemas/basic/line.py +++ b/src/vuecore/schemas/basic/line.py @@ -1,10 +1,12 @@ -from typing import Dict, List, Optional -from pydantic import BaseModel, Field, ConfigDict +from typing import Dict, Optional +from pydantic import Field, ConfigDict +from vuecore.schemas.plotly_base import PlotlyBaseConfig -class LineConfig(BaseModel): +class LineConfig(PlotlyBaseConfig): """ - Pydantic model for validating and managing line plot configurations. + Pydantic model for validating and managing line plot configurations, + which extends PlotlyBaseConfig. This model serves as a curated API for the most relevant parameters for line plots, closely aligned with the `plotly.express.line` API @@ -16,71 +18,6 @@ class LineConfig(BaseModel): defined here, and also accepts additional Plotly keyword arguments, forwarding them to the appropriate `plotly.express.line` or `plotly.graph_objects.Figure` call. - - Attributes - ---------- - -----Data Mapping----- - x : str - Column for the x-axis values. - y : str - Column for the y-axis values. - line_group : Optional[str] - Column to group data into separate lines. - color : Optional[str] - Column to assign color to lines. Replaces 'group'. - line_dash : Optional[str] - Column to assign dash styles to lines. - symbol : Optional[str] - Column to assign symbols to markers. - hover_name : Optional[str] - Column to appear in bold in the hover tooltip. - hover_data : Optional[List[str]] - Additional columns to display in the hover tooltip. - text : Optional[str] - Column for adding text labels to markers. - facet_row : Optional[str] - Column to create vertical subplots (facets). - facet_col : Optional[str] - Column to create horizontal subplots (facets). - error_x : Optional[str] - Column for sizing x-axis error bars. - error_y : Optional[str] - Column for sizing y-axis error bars. - labels : Optional[Dict[str, str]] - Dictionary to override column names for titles, legends, etc. - color_discrete_map : Optional[Dict[str, str]] - Specific color mappings for values in the `color` column. - line_dash_map : Optional[Dict[str, str]] - Specific dash style mappings for values in the `line_dash` column. - symbol_map : Optional[Dict[str, str]] - Specific symbol mappings for values in the `symbol` column. - -----Styling and Layout----- - markers : bool - If True, markers are drawn on the lines. - log_x : bool - If True, the x-axis is log-scaled. - log_y : bool - If True, the y-axis is log-scaled. - range_x : Optional[List[float]] - Range for the x-axis, e.g., [0, 100]. - range_y : Optional[List[float]] - Range for the y-axis, e.g., [0, 100]. - line_shape : Optional[str] - Determines the line shape ('linear', 'spline', 'hv', etc.). - title : str - The main title of the plot. - x_title : Optional[str] - Custom title for the x-axis. - y_title : Optional[str] - Custom title for the y-axis. - subtitle : str - The subtitle of the plot. - template : str - Plotly template for styling (e.g., 'plotly_white'). - width : int - Width of the plot in pixels. - height : int - Height of the plot in pixels. """ # General Configuration @@ -88,39 +25,18 @@ class LineConfig(BaseModel): model_config = ConfigDict(extra="allow") # Data Mapping - x: str = Field(..., description="Column for x-axis values.") - y: str = Field(..., description="Column for y-axis values.") line_group: Optional[str] = Field( None, description="Column to group data into separate lines." ) - color: Optional[str] = Field(None, description="Column to assign color to lines.") line_dash: Optional[str] = Field( None, description="Column to assign dash styles to lines." ) symbol: Optional[str] = Field( None, description="Column to assign symbols to markers." ) - hover_name: Optional[str] = Field( - None, description="Column for bold text in hover tooltip." - ) - hover_data: List[str] = Field( - [], description="Additional columns for the hover tooltip." - ) text: Optional[str] = Field(None, description="Column for text labels on markers.") - facet_row: Optional[str] = Field( - None, description="Column to create vertical subplots." - ) - facet_col: Optional[str] = Field( - None, description="Column to create horizontal subplots." - ) error_x: Optional[str] = Field(None, description="Column for x-axis error bars.") error_y: Optional[str] = Field(None, description="Column for y-axis error bars.") - labels: Optional[Dict[str, str]] = Field( - None, description="Override column names in the plot." - ) - color_discrete_map: Optional[Dict[str, str]] = Field( - None, description="Map values to specific colors." - ) line_dash_map: Optional[Dict[str, str]] = Field( None, description="Map values to specific dash styles." ) @@ -130,21 +46,6 @@ class LineConfig(BaseModel): # Styling and Layout markers: bool = Field(False, description="If True, displays markers on the lines.") - log_x: bool = Field(False, description="If True, use a logarithmic x-axis.") - log_y: bool = Field(False, description="If True, use a logarithmic y-axis.") - range_x: Optional[List[float]] = Field( - None, description="Range for the x-axis, e.g., [0, 100]." - ) - range_y: Optional[List[float]] = Field( - None, description="Range for the y-axis, e.g., [0, 100]." - ) line_shape: Optional[str] = Field( "linear", description="Line shape (e.g., 'linear', 'spline')." ) - title: str = Field("Line Plot", description="The main title of the plot.") - x_title: Optional[str] = Field(None, description="Custom title for the x-axis.") - y_title: Optional[str] = Field(None, description="Custom title for the y-axis.") - subtitle: Optional[str] = Field(None, description="The subtitle for the plot.") - template: str = Field("plotly_white", description="Plotly template for styling.") - width: int = Field(800, description="Width of the plot in pixels.") - height: int = Field(600, description="Height of the plot in pixels.") diff --git a/src/vuecore/schemas/basic/scatter.py b/src/vuecore/schemas/basic/scatter.py index 19d67b1..deb1e56 100644 --- a/src/vuecore/schemas/basic/scatter.py +++ b/src/vuecore/schemas/basic/scatter.py @@ -1,8 +1,9 @@ -from typing import Dict, List, Optional -from pydantic import BaseModel, Field, ConfigDict, model_validator +from typing import Dict, Optional +from pydantic import Field, ConfigDict, model_validator +from vuecore.schemas.plotly_base import PlotlyBaseConfig -class ScatterConfig(BaseModel): +class ScatterConfig(PlotlyBaseConfig): """ Pydantic model for validating and managing scatter plot configurations. @@ -16,142 +17,31 @@ class ScatterConfig(BaseModel): defined here, and also accepts additional Plotly keyword arguments, forwarding them to the appropriate `plotly.express.scatter` or `plotly.graph_objects.Figure` call. - - Attributes - --------- - -----Data Mapping----- - x : str - Column name for x-axis values. - y : str - Column name for y-axis values. - color : Optional[str] - Column to assign color to markers. - symbol : Optional[str] - Column to assign marker symbols. - size : Optional[str] - Column to determine marker size. - hover_name : Optional[str] - Column for bold text in hover tooltip. - hover_data : List[str] - Additional columns for hover tooltip. - text : Optional[str] - Column for text labels on markers. - facet_row : Optional[str] - Column for vertical facetting. - facet_col : Optional[str] - Column for horizontal facetting. - error_x : Optional[str] - Column for x-axis error bars. - error_y : Optional[str] - Column for y-axis error bars. - labels : Optional[Dict[str, str]] - Column name overrides for display. - color_discrete_map : Optional[Dict[str, str]] - Specific color mappings for color column values. - symbol_map : Optional[Dict[str, str]] - Specific symbol mappings for symbol column values. - size_max : int - Maximum marker size. - -----Styling and Layout----- - opacity : float - Marker opacity (0-1). - trendline : Optional[str] - Trendline type (ols/lowess/rolling/expanding/ewm). - trendline_options : Optional[Dict] - Advanced options for trendlines. - log_x : bool - Enable logarithmic x-axis. - log_y : bool - Enable logarithmic y-axis. - range_x : Optional[List[float]] - Manual x-axis range [min, max]. - range_y : Optional[List[float]] - Manual y-axis range [min, max]. - title : str - Main plot title. - x_title : Optional[str] - Custom title for the x-axis. - y_title : Optional[str] - Custom title for the y-axis. - subtitle : Optional[str] - Plot subtitle. - template : str - Plotly visual theme/template. - width : int - Plot width in pixels. - height : int - Plot height in pixels. - color_by_density : bool - Color points by density instead of category. - marker_line_width : float - Width of marker border lines. - marker_line_color : str - Color of marker border lines. """ # General Configuration # Allow extra parameters to pass through to Plotly model_config = ConfigDict(extra="allow") - # Data mapping - x: str = Field(..., description="Column for x-axis values.") - y: str = Field(..., description="Column for y-axis values.") - color: Optional[str] = Field( - None, description="Column for color assignment (replaces 'group')." - ) - symbol: Optional[str] = Field( - None, description="Column for marker symbol assignment." - ) + # Data Mapping + symbol: Optional[str] = Field(None, description="Column to assign marker symbols.") size: Optional[str] = Field(None, description="Column to determine marker size.") - hover_name: Optional[str] = Field( - None, description="Column for bold text in hover tooltip." - ) - hover_data: List[str] = Field( - [], description="Additional columns for hover tooltip." - ) text: Optional[str] = Field(None, description="Column for text labels on markers.") - facet_row: Optional[str] = Field(None, description="Column for vertical facetting.") - facet_col: Optional[str] = Field( - None, description="Column for horizontal facetting." - ) error_x: Optional[str] = Field(None, description="Column for x-axis error bars.") error_y: Optional[str] = Field(None, description="Column for y-axis error bars.") - labels: Optional[Dict[str, str]] = Field( - None, description="Column name overrides for display purposes." - ) - color_discrete_map: Optional[Dict[str, str]] = Field( - None, description="Specific color mappings for color column values." - ) symbol_map: Optional[Dict[str, str]] = Field( None, description="Specific symbol mappings for symbol column values." ) size_max: int = Field(20, description="Maximum size for markers.") # Styling and Layout - opacity: float = Field(0.8, ge=0, le=1, description="Overall opacity of markers.") + opacity: float = Field(0.8, description="Overall opacity of markers.") trendline: Optional[str] = Field( None, description="Trendline type (ols/lowess/rolling/expanding/ewm)." ) trendline_options: Optional[Dict] = Field( None, description="Advanced options for trendline configuration." ) - log_x: bool = Field(False, description="Enable logarithmic x-axis scale.") - log_y: bool = Field(False, description="Enable logarithmic y-axis scale.") - range_x: Optional[List[float]] = Field( - None, description="Manual x-axis range [min, max]." - ) - range_y: Optional[List[float]] = Field( - None, description="Manual y-axis range [min, max]." - ) - title: str = Field("Scatter Plot", description="Main title of the plot.") - subtitle: Optional[str] = Field( - None, description="Subtitle displayed below main title." - ) - x_title: Optional[str] = Field(None, description="Custom title for the x-axis.") - y_title: Optional[str] = Field(None, description="Custom title for the y-axis.") - template: str = Field("plotly_white", description="Plotly visual theme/template.") - width: int = Field(800, description="Plot width in pixels.") - height: int = Field(600, description="Plot height in pixels.") marker_line_width: float = Field( 0.5, ge=0, description="Width of marker border lines." ) @@ -168,7 +58,7 @@ class ScatterConfig(BaseModel): def validate_exclusive_color_options(self) -> "ScatterConfig": if self.color_by_density and self.color: raise ValueError( - "Cannot use both 'color' and 'color_by_density'. " - "These options are mutually exclusive." + "Cannot use both 'color_by_density' and 'color' parameters. " + "Please choose only one for coloring the markers." ) return self diff --git a/src/vuecore/schemas/plotly_base.py b/src/vuecore/schemas/plotly_base.py new file mode 100644 index 0000000..d1c7369 --- /dev/null +++ b/src/vuecore/schemas/plotly_base.py @@ -0,0 +1,74 @@ +from typing import Dict, List, Optional +from pydantic import BaseModel, Field, ConfigDict, model_validator + + +class PlotlyBaseConfig(BaseModel): + """ + Pydantic model for common Plotly configurations. + + This model serves as a curated API for common parameters of Plotly plots, + closely aligned with the `plotly.express` API + (https://plotly.com/python-api-reference/plotly.express.html). + + This base class includes parameters shared across multiple plot types + to ensure consistency and reduce code repetition. It uses a validator to + enforce that at least one of the x or y axes is provided. Plot-specific + schemas should inherit from this model. + """ + + model_config = ConfigDict(extra="allow") + + # Data Mapping + x: Optional[str] = Field(None, description="Column for x-axis values.") + y: Optional[str] = Field(None, description="Column for y-axis values.") + color: Optional[str] = Field( + None, description="Column to assign color to plot elements." + ) + hover_name: Optional[str] = Field( + None, description="Column to appear in bold in the hover tooltip." + ) + hover_data: List[str] = Field( + [], description="Additional columns for the hover tooltip." + ) + facet_row: Optional[str] = Field( + None, description="Column to create vertical subplots (facets)." + ) + facet_col: Optional[str] = Field( + None, description="Column to create horizontal subplots (facets)." + ) + labels: Optional[Dict[str, str]] = Field( + None, + description="Dictionary to override column names for titles, legends, etc.", + ) + color_discrete_map: Optional[Dict[str, str]] = Field( + None, description="Specific color mappings for values in the `color` column." + ) + category_orders: Optional[Dict[str, List[str]]] = Field( + None, description="Dictionary to specify the order of categorical values." + ) + + # Styling and Layout + log_x: bool = Field(False, description="If True, use a logarithmic x-axis.") + log_y: bool = Field(False, description="If True, use a logarithmic y-axis.") + range_x: Optional[List[float]] = Field( + None, description="Range for the x-axis, e.g., [0, 100]." + ) + range_y: Optional[List[float]] = Field( + None, description="Range for the y-axis, e.g., [0, 100]." + ) + title: str = Field("Plotly Plot", description="The main title of the plot.") + x_title: Optional[str] = Field(None, description="Custom title for the x-axis.") + y_title: Optional[str] = Field(None, description="Custom title for the y-axis.") + subtitle: Optional[str] = Field(None, description="The subtitle of the plot.") + template: str = Field("plotly_white", description="Plotly template for styling.") + width: Optional[int] = Field(800, description="Width of the plot in pixels.") + height: Optional[int] = Field(600, description="Height of the plot in pixels.") + + @model_validator(mode="after") + def validate_x_or_y_provided(self) -> "PlotlyBaseConfig": + """Ensure at least one of x or y is provided for the plot.""" + if self.x is None and self.y is None: + raise ValueError( + "At least one of 'x' or 'y' must be provided for the plot." + ) + return self diff --git a/src/vuecore/utils/docs_utils.py b/src/vuecore/utils/docs_utils.py new file mode 100644 index 0000000..8ebf563 --- /dev/null +++ b/src/vuecore/utils/docs_utils.py @@ -0,0 +1,144 @@ +# vuecore/utils/doc_utils.py +from typing import Type, Dict, Any, get_origin, get_args, Optional, Union +from pydantic import BaseModel, Field +from functools import wraps +import textwrap +import re + + +def get_all_model_fields(model: Type[BaseModel]) -> Dict[str, Field]: + """ + Extract all fields from a Pydantic model, including inherited ones. + + Parameters + ---------- + model : Type[BaseModel] + The Pydantic model class from which to extract fields. + + Returns + ------- + Dict[str, Any] + A dictionary with field names as keys and field information as values. + """ + fields = {} + # Iterate through the method resolution order (mro) in reverse to start from the base classes + for cls in reversed(model.__mro__): + if issubclass(cls, BaseModel) and hasattr(cls, "model_fields"): + for field_name, field in cls.model_fields.items(): + fields[field_name] = field + return fields + + +def get_type_string(annotation: Any) -> str: + """ + Helper to get a clean type string from a type annotation. + + Parameters + ---------- + annotation : Any + The type annotation to process. + + Returns + ------- + str + A simplified string representation of the type. + """ + origin = get_origin(annotation) + if origin is Union or origin is Optional: + args = get_args(annotation) + non_none_arg = next((arg for arg in args if arg is not type(None)), None) + if non_none_arg: + return get_type_string(non_none_arg) + return "Any" + elif origin is list or annotation is list: + args = get_args(annotation) + if not args: + return "list" + inner_type = ", ".join(arg.__name__ for arg in args) + return f"list of {inner_type}" + elif origin is dict or annotation is dict: + args = get_args(annotation) + if not args: + return "dict" + key_type_str = get_type_string(args[0]) + value_type_str = get_type_string(args[1]) + return f"Dict[{key_type_str}, {value_type_str}]" + return annotation.__name__ + + +def document_pydant_params(model: Type[BaseModel]): + """ + Decorator to add Pydantic model parameters to a function's docstrings. + + Parameters + ---------- + model : Type[BaseModel] + The Pydantic model class whose fields should be documented. + + Returns + ------- + function + A decorator function that modifies the docstring of the target function. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + original_doc = wrapper.__doc__ or "" + lines = original_doc.splitlines() + + # Find the end of the "Parameters" section + insert_index = -1 + # Find the line that marks the end of the Parameters section + # or the beginning of a new section like Returns or Raises + for i, line in enumerate(lines): + if re.match(r"^\s*(Returns|Raises|Examples|See Also)", line): + insert_index = i + break + + # If no other section is found, assume the end of the docstring + if insert_index == -1: + insert_index = len(lines) + + # Generate the new, detailed parameter list in NumPydocs style + fields = get_all_model_fields(model) + params_doc_lines = [] + for field_name, field in fields.items(): + type_str = get_type_string(field.annotation) + description = field.description or "No description available." + default_value = field.get_default(call_default_factory=True) + default_str = ( + f" (default: ``{default_value}``)" + if default_value is not None and default_value is not ... + else "" + ) + + # Format as a single line for the NumPy-style bulleted list + params_doc_lines.append( + f"* **{field_name}** ({type_str}) – {description}{default_str}" + ) + + # Indent all lines in the parameter list correctly + indented_params_lines = textwrap.indent( + "\n".join(params_doc_lines), prefix=" " + ).splitlines() + + # Create the kwargs documentation as a list of lines + new_kwargs_lines = [ + " **kwargs", + " Keyword arguments for plot configuration. These arguments are validated against", + f" the ``{model.__name__}`` Pydantic model and the engine specific parameters.\n", + " The following parameters are supported:\n", + ] + + # Add all parameter lines + new_kwargs_lines.extend(indented_params_lines) + + # Reconstruct the docstring by inserting the new lines + new_doc_lines = lines[:insert_index] + new_kwargs_lines + lines[insert_index:] + wrapper.__doc__ = "\n".join(new_doc_lines) + return wrapper + + return decorator diff --git a/src/vuecore/utils/validation.py b/src/vuecore/utils/validation.py deleted file mode 100644 index 8b13789..0000000 --- a/src/vuecore/utils/validation.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/test_docs_utils.py b/tests/test_docs_utils.py new file mode 100644 index 0000000..4de8ad1 --- /dev/null +++ b/tests/test_docs_utils.py @@ -0,0 +1,112 @@ +import pytest +import textwrap +from pydantic import BaseModel, Field +from typing import Optional, List, Dict + +# Import the decorator and helper functions from your module +from vuecore.utils.docs_utils import ( + document_pydant_params, + get_type_string, + get_all_model_fields, +) + + +# --- Test Model and Function --- +# Pydantic model with different field types for validation +class TestModel(BaseModel): + """A test model for decorator documentation.""" + + param_str: str = Field(..., description="A string parameter.") + param_int: int = Field(5, description="An integer parameter with a default.") + param_list: Optional[List[str]] = Field( + None, description="An optional list of strings." + ) + param_dict: Dict = Field({}, description="A generic dictionary parameter.") + param_bare_list: list = Field([], description="A bare list parameter.") + + +# Function to apply the decorator to and check its docstring +@document_pydant_params(TestModel) +def dummy_plot_function(data: dict): + """ + This is the original docstring. + + Parameters + ---------- + data : dict + A dictionary of sample data. + + Returns + ------- + None + Does not return anything. + """ + pass + + +# --- Tests for Helper Functions --- +def test_get_all_model_fields(): + """Verify that all fields, including inherited ones, are correctly extracted.""" + fields = get_all_model_fields(TestModel) + assert "param_str" in fields + assert "param_int" in fields + assert "param_list" in fields + assert "param_dict" in fields + assert "param_bare_list" in fields + assert len(fields) == 5 + + +@pytest.mark.parametrize( + "annotation, expected_string", + [ + (str, "str"), + (int, "int"), + (Optional[str], "str"), + (List[str], "list of str"), + (Dict, "dict"), + (list, "list"), + (Dict[str, int], "Dict[str, int]"), + ], +) +def test_get_type_string(annotation, expected_string): + """Test that the type string is correctly formatted for various types.""" + assert get_type_string(annotation) == expected_string + + +# --- Test for the Decorator --- +def test_decorator_modifies_docstring(): + """Verify the decorator correctly adds model parameters to the docstring.""" + + # Get the decorated function's docstring + decorated_docstring = dummy_plot_function.__doc__ + + # Assert that the core parts of the original and generated docstrings are present + assert "This is the original docstring." in decorated_docstring + assert "Parameters" in decorated_docstring + assert "data : dict" in decorated_docstring + assert "Returns" in decorated_docstring + assert "Does not return anything." in decorated_docstring + + # Assert that the kwargs section and its description are present + assert "**kwargs" in decorated_docstring + assert "Keyword arguments for plot configuration." in decorated_docstring + assert "``TestModel`` Pydantic model" in decorated_docstring + + # Assert that each parameter is present with its correct type and default value + assert "* **param_str** (str) – A string parameter." in decorated_docstring + assert ( + "* **param_int** (int) – An integer parameter with a default. (default: ``5``)" + in decorated_docstring + ) + assert ( + "* **param_list** (list of str) – An optional list of strings." + in decorated_docstring + ) + assert ( + "* **param_dict** (dict) – A generic dictionary parameter. (default: ``{}``)" + in decorated_docstring + ) + assert ( + "* **param_bare_list** (list) – A bare list parameter. (default: ``[]``)" + in decorated_docstring + )