From a3f79babb667e193e14374904e5a7db4e259c3c3 Mon Sep 17 00:00:00 2001 From: Julien Barnier Date: Fri, 24 May 2024 17:30:31 +0200 Subject: [PATCH] style: convert docstrings to numpy format --- src/pyobsplot/data.py | 51 +++++---- src/pyobsplot/js_modules.py | 33 ++++-- src/pyobsplot/jsdom.py | 47 +++++--- src/pyobsplot/obsplot.py | 210 +++++++++++++++++++++++++----------- src/pyobsplot/parsing.py | 108 ++++++++++++------- src/pyobsplot/widget.py | 36 ++++--- 6 files changed, 320 insertions(+), 165 deletions(-) diff --git a/src/pyobsplot/data.py b/src/pyobsplot/data.py index 1a7daa18..1f977ddc 100644 --- a/src/pyobsplot/data.py +++ b/src/pyobsplot/data.py @@ -8,20 +8,26 @@ import pandas as pd import polars as pl -import pyarrow as pa import pyarrow.feather as pf def serialize(data: Any, renderer: str) -> Any: - """Serialize a data object. + """ + Serialize a data object. - Args: - data (Any): data object to serialize. - renderer (str): renderer. + Parameters + ---------- + data : Any + data object to serialize. + renderer : str + renderer type. - Returns: - Any: serialized data object. + Returns + ------- + Any + serialized data object. """ + # If polars DataFrame, serialize to Arrow IPC if isinstance(data, pl.DataFrame): value = pl_to_arrow(data) @@ -40,13 +46,18 @@ def serialize(data: Any, renderer: str) -> Any: def pd_to_arrow(df: pd.DataFrame) -> bytes: - """Convert a pandas DataFrame to Arrow IPC bytes. + """ + Convert a pandas DataFrame to Arrow IPC bytes. - Args: - df (pd.DataFrame): pandas DataFrame to convert. + Parameters + ---------- + df : pd.DataFrame + pandas DataFrame to convert. - Returns: - bytes: Arrow IPC bytes + Returns + ------- + bytes + Arrow IPC bytes. """ f = io.BytesIO() df.to_feather(f, compression="uncompressed") @@ -54,15 +65,19 @@ def pd_to_arrow(df: pd.DataFrame) -> bytes: def pl_to_arrow(df: pl.DataFrame) -> bytes: - """Convert a polars DataFrame to Arrow IPC bytes. + """ + Convert a polars DataFrame to Arrow IPC bytes. - Args: - df (pl.DataFrame): polars DataFrame to convert. + Parameters + ---------- + df : pl.DataFrame + polars DataFrame to convert. - Returns: - bytes: Arrow IPC bytes. + Returns + ------- + bytes + Arrow IPC bytes. """ - f = io.BytesIO() pf.write_feather(df.to_arrow(), f, compression="uncompressed") return f.getvalue() diff --git a/src/pyobsplot/js_modules.py b/src/pyobsplot/js_modules.py index 542ca5f6..a2cf479b 100644 --- a/src/pyobsplot/js_modules.py +++ b/src/pyobsplot/js_modules.py @@ -1,5 +1,5 @@ from functools import partial -from typing import Callable, Optional +from typing import Callable from pyobsplot.obsplot import ObsplotJsdomCreator, ObsplotWidgetCreator from pyobsplot.utils import PLOT_METHODS @@ -11,10 +11,12 @@ class Plot: - """Plot methods class.""" + """ + Plot methods class. + """ @staticmethod - def plot(*args, **kwargs) -> Optional[ObsplotWidget]: + def plot(*args, **kwargs) -> ObsplotWidget | None: """ Plot.plot static method. If called directly, create an ObsplotWidget or an ObpsplotJsdom with args and kwargs. @@ -29,11 +31,14 @@ def plot(*args, **kwargs) -> Optional[ObsplotWidget]: def method_to_spec(*args, **kwargs) -> dict: - """Function used for creating Plot.xyz static methods. + """ + Function used for creating Plot.xyz static methods. Generates a dict of specification with method name and args. - Returns: - dict: Plot function specification. + Returns + ------- + dict + Plot function specification. """ name = kwargs["name"] if len(kwargs) > 1: @@ -55,10 +60,14 @@ def method_to_spec(*args, **kwargs) -> dict: class JSModule(type): - """metaclass to allow JavaScript module and methods handling.""" + """ + Metaclass to allow JavaScript module and methods handling. + """ def __getattr__(cls: type, name: str) -> Callable: - """Intercept methods calling and returns a parsed and typed dict object.""" + """ + Intercept methods calling and returns a parsed and typed dict object. + """ def wrapper(*args, **kwargs) -> dict: if kwargs: @@ -75,12 +84,16 @@ def wrapper(*args, **kwargs) -> dict: class d3(metaclass=JSModule): # noqa: N801 - """JSModule class to allow d3 objects in specification.""" + """ + JSModule class to allow d3 objects in specification. + """ pass class Math(metaclass=JSModule): - """JSModule class to allow Math objects in specification.""" + """ + JSModule class to allow Math objects in specification. + """ pass diff --git a/src/pyobsplot/jsdom.py b/src/pyobsplot/jsdom.py index cda3756e..b15633a8 100644 --- a/src/pyobsplot/jsdom.py +++ b/src/pyobsplot/jsdom.py @@ -4,7 +4,7 @@ import json import warnings -from typing import Any, Optional, Union +from typing import Any import requests from IPython.display import HTML, SVG @@ -16,26 +16,34 @@ class ObsplotJsdom: - """Obsplot JSDom class. - - The class takes a plot specification as input and generates a plot as SVG or HTML - by calling a JSDom script with node. - - The specification can be given as a dict, a Plot function call or as - Python kwargs. - """ def __init__( self, + *, spec: Any, port: int, theme: str = DEFAULT_THEME, - default: Optional[dict] = None, - debug: bool = False, # noqa: FBT002, FBT001 + default: dict | None = None, + debug: bool = False, ) -> None: """ - Constructor. Parse the spec given as argument. + Obsplot JSDom class. The class takes a plot specification as input and generates + a plot as SVG or HTML by calling a JSDom script with node. + + Parameters + ---------- + spec : Any + Plot specification as dict, Plot function call or Python kwargs. + port : int + port number of the jsdom server. + theme : {'light', 'dark', 'current'}, optional + color theme to use, by default 'light' + default : dict, optional + dict of default spec values, by default None + debug : bool, optional + activate debug mode, by default False """ + # Create parser parser = SpecParser(renderer="jsdom", default=default) # Parse spec code @@ -47,10 +55,13 @@ def __init__( self.port = port self.theme = theme - def plot(self) -> Union[SVG, HTML]: - """Generates the plot by sending request to http node server. + def plot(self) -> SVG | HTML: + """ + Generates the plot by sending request to http node server. - Returns: + Returns + ------- + HTML | SVG Either an HTML or SVG IPython.display object. """ @@ -63,8 +74,10 @@ def plot(self) -> Union[SVG, HTML]: timeout=600, ) except ConnectionRefusedError: - msg = f"""Error: can't connect to generator server on port {self.port}. - Please recreate your generator object.""" + msg = ( + f"Error: can't connect to generator server on port {self.port}.\n" + f"Please recreate your generator object." + ) warnings.warn(msg, stacklevel=1) # Read back result if r.status_code == HTTP_SERVER_ERROR: # type: ignore diff --git a/src/pyobsplot/obsplot.py b/src/pyobsplot/obsplot.py index cdb23dab..8a73ca69 100644 --- a/src/pyobsplot/obsplot.py +++ b/src/pyobsplot/obsplot.py @@ -2,6 +2,8 @@ Obsplot main class. """ +from __future__ import annotations + import io import os import shutil @@ -10,7 +12,7 @@ import warnings from pathlib import Path from subprocess import PIPE, Popen, SubprocessError -from typing import Any, Optional, Union +from typing import Literal import typst from IPython.display import HTML, SVG, Image, display @@ -28,39 +30,41 @@ class Obsplot: - """ - Main Obsplot class. - - Launches a Jupyter widget with ObsplotWidget class, or displays an IPython display - with ObsplotJsdom depending on the renderer. - """ def __new__( cls, - renderer: str = "widget", + renderer: Literal["widget", "jsdom"] | None = "widget", *, - theme: str = DEFAULT_THEME, - default: Optional[dict] = None, - format: Optional[str] = None, # noqa: A002 - format_options: Optional[dict] = None, + theme: Literal["light", "dark", "current"] = DEFAULT_THEME, + default: dict | None = None, + format: Literal["html", "svg", "png"] | None = None, # noqa: A002 + format_options: dict | None = None, debug: bool = False, - ) -> Any: + ) -> ObsplotCreator: """ - Main Obsplot class constructor. Returns a Creator instance depending on the + Main Obsplot class. Returns a Creator instance depending on the renderer passed as argument. - Args: - renderer (str): renderer to be used. - theme (str): color theme to use, can be "light" (default), "dark" or - "current". - default (dict): dict of default spec values. - format (str): default output display format for jsdom renderer, can be - "html", "svg", "png" or "pdf". - format_options (dict): default options passed to typst when converting - to png or pdf. - debug (bool): if True, activate debug mode (for widget renderer only) - - returns: + Parameters + ---------- + renderer : {'widget', 'jsdom'}, optional + renderer to be used, by default "widget" + theme : {'light', 'dark', 'current'}, optional + color theme to use, by default 'light' + default : dict, optional + dict of default spec values, by default None + format : {'html', 'svg', 'png'}, optional + default output format for jsdom renderer, by default None + format_options : dict, optional + default output format options for typst formatter. Currently + possible keys are 'font' (name of font family), 'scale' (font scaling) + and 'margin' (margin in pt around the plot) + debug : bool, optional + activate debug mode, by default False + + Returns + ------- + ObsplotCreator A Creator object of type depending of the renderer. """ @@ -91,7 +95,7 @@ def __new__( raise ValueError(msg) return ObsplotWidgetCreator(theme=theme, default=default, debug=debug) - elif renderer == "jsdom": + else: if format_options is None: format_options = {} return ObsplotJsdomCreator( @@ -104,21 +108,27 @@ def __new__( class ObsplotCreator: - """ - Creator class. - """ def __init__( self, - theme: str = DEFAULT_THEME, - default: Optional[dict] = None, - debug: bool = False, # noqa: FBT001, FBT002 + *, + theme: Literal["light", "dark", "current"] = DEFAULT_THEME, + default: dict | None = None, + debug: bool = False, ) -> None: - """Generic Creator constructor - - Args: - default (dict, optional): dict of default spec values. Defaults to {}. """ + Generic Creator constructor. + + Parameters + ---------- + theme : {'light', 'dark', 'current'}, optional + color theme to use, by default 'light' + default : dict, optional + dict of default spec values, by default None + debug : bool, optional + activate debug mode, by default False + """ + if default is None: default = {} for k in default: @@ -126,8 +136,8 @@ def __init__( msg = f"{k} is not allowed in default.\nAllowed values: {ALLOWED_DEFAULTS}." # noqa: E501 raise ValueError(msg) self._default = default - self._debug = debug self._theme = theme + self._debug = debug def __repr__(self): return ( @@ -179,29 +189,52 @@ def get_spec(self, *args, **kwargs): class ObsplotWidgetCreator(ObsplotCreator): - """ - Widget renderer Creator class. - """ def __init__( self, - theme: str = DEFAULT_THEME, - default: Optional[dict] = None, - debug: bool = False, # noqa: FBT001, FBT002 + *, + theme: Literal["light", "dark", "current"] = DEFAULT_THEME, + default: dict | None = None, + debug: bool = False, ) -> None: - super().__init__(theme, default, debug) + """ + Widget renderer Creator class. + + Parameters + ---------- + theme : {'light', 'dark', 'current'}, optional + color theme to use, by default 'light' + default : dict, optional + dict of default spec values, by default None + debug : bool, optional + activate debug mode (for widget renderer only), by default False + """ - def __call__(self, *args, **kwargs) -> Optional[ObsplotWidget]: + super().__init__(theme=theme, default=default, debug=debug) + + def __call__(self, *args, **kwargs) -> ObsplotWidget | None: """ - Method called when an instance is called. + Method called when an instance is called directly. + + Parameters + ---------- + path : str, optional + if provided, plot is saved to disk to an HTML file instead of displayed + as a jupyter widget. + + Returns + ------- + Optional[ObsplotWidget] + An ObsplotWidget widget object if path is not defined, otherwse None. """ + path = None if "path" in kwargs: path = kwargs["path"] del kwargs["path"] spec = self.get_spec(*args, **kwargs) res = ObsplotWidget( - spec, theme=self._theme, default=self._default, debug=self._debug + spec=spec, theme=self._theme, default=self._default, debug=self._debug ) # type: ignore if path is not None: ObsplotWidgetCreator.save_to_file(path, res) @@ -213,9 +246,12 @@ def save_to_file(path: str, res: ObsplotWidget) -> None: """ Save an Obsplot object generated by a widget creator to a file. - Args: - path (str): path to output file. - res (ObsplotWidget): result of a call to Obsplot(). + Parameters + ---------- + path : str + path to output file. Must have an "html" extension + res : ObsplotWidget + widget object to be saved to disk """ extension = Path(path).suffix.lower() if extension not in [".html", ".htm"]: @@ -228,21 +264,36 @@ def save_to_file(path: str, res: ObsplotWidget) -> None: class ObsplotJsdomCreator(ObsplotCreator): - """ - Jsdom renderer Creator class. - """ def __init__( self, *, - theme: str = DEFAULT_THEME, - default: Optional[dict] = None, - format: Optional[str] = None, # noqa: A002 - format_options: Optional[dict] = None, + theme: Literal["light", "dark", "current"] = DEFAULT_THEME, + default: dict | None = None, + format: Literal["html", "svg", "png"] | None = None, # noqa: A002 + format_options: dict | None = None, debug: bool = False, ) -> None: + """ + Jsdom renderer Creator class. + + Parameters + ---------- + theme : {'light', 'dark', 'current'}, optional + color theme to use, by default 'light' + default : dict, optional + dict of default spec values, by default None + format : {'html', 'svg', 'png'}, optional + default output format for jsdom renderer, by default None + format_options : dict, optional + default output format options for typst formatter. Currently + possible keys are 'font' (name of font family), 'scale' (font scaling) + and 'margin' (margin in pt around the plot) + debug : bool, optional + activate debug mode (for widget renderer only), by default False + """ - super().__init__(theme, default, debug) + super().__init__(theme=theme, default=default, debug=debug) allowed_formats = ["html", "svg", "png"] if format is not None and format not in allowed_formats: @@ -360,7 +411,7 @@ def __call__( spec["figure"] = True res = ObsplotJsdom( - spec, + spec=spec, port=self._port, theme=self._theme, default=self._default, @@ -370,18 +421,47 @@ def __call__( if format in ["png", "pdf"] or format == "svg" and isinstance(res, HTML): if format == "svg" and isinstance(res, HTML): warnings.warn( - f"HTML figure converted to SVG via typst.", + "HTML figure converted to SVG via typst.", RuntimeWarning, stacklevel=1, ) - res = self.typst_render(res, format, format_options) + res = self.typst_render(res, format, format_options) # type: ignore if path is None: display(res) else: ObsplotJsdomCreator.save_to_file(path, res) # type: ignore - def typst_render(self, res, format, options) -> SVG | Image | bytes: # noqa: A002 + def typst_render( + self, + figure: HTML, + format: Literal["pdf", "svg", "png"], # noqa: A002 + options: dict | None = None, + ) -> SVG | Image | bytes: + """ + Run an HTML jsdom output through typst for conversion to png, pdf or svg. + + Parameters + ---------- + figure : HTML + output of jsdom renderer (HTML) + format : {'png', 'pdf', 'svg'} + format of output to generate. + options : dict, optional + + Returns + ------- + _type_ + _description_ + + Raises + ------ + ValueError + _description_ + """ + + if options is None: + options = {} if format not in ["png", "pdf", "svg"]: msg = f"Invalid format: {format}." @@ -394,7 +474,7 @@ def typst_render(self, res, format, options) -> SVG | Image | bytes: # noqa: A0 # Write HTML jsdom output to file with open(tmpdir / "jsdom.html", "w") as jsdom_out: - jsdom_out.write(res.data) + jsdom_out.write(str(figure.data)) # Copy typst template shutil.copy(bundler_output_dir / "template.typ", tmpdir / "template.typ") # Create the typst input file @@ -427,7 +507,7 @@ def typst_render(self, res, format, options) -> SVG | Image | bytes: # noqa: A0 return res @staticmethod - def save_to_file(path: str, res: Union[SVG, HTML, Image]) -> None: + def save_to_file(path: str, res: SVG | HTML | Image) -> None: """ Save an Obsplot object generated by a Jsdom creator to a file. diff --git a/src/pyobsplot/parsing.py b/src/pyobsplot/parsing.py index 14604756..efe1c409 100644 --- a/src/pyobsplot/parsing.py +++ b/src/pyobsplot/parsing.py @@ -4,7 +4,7 @@ import datetime import json -from typing import Any, Optional +from typing import Any, Literal import pandas as pd import polars as pl @@ -13,19 +13,21 @@ class SpecParser: - """ - Class implementing plot specification parsing. - """ def __init__( - self, renderer: str = "widget", default: Optional[dict] = None + self, + renderer: Literal["widget", "jsdom"] = "widget", + default: dict | None = None, ) -> None: """ - SpecParser constructor. - - Args: - renderer(str): type of renderer ("widget" or "jsdom"). - default(dict): dict of default spec values. + Class implementing plot specification parsing. + + Parameters + ---------- + renderer : {'widget', 'jsdom'} + type of renderer. + default : dict + dict of default spec values. """ self.renderer = renderer self.data = [] @@ -48,14 +50,19 @@ def spec(self, value): value = {"marks": [value]} self._spec = value - def cache_index(self, data: Any) -> Optional[int]: - """Returns the index of a data object in the data cache. + def cache_index(self, data: Any) -> int | None: + """ + Returns the index of a data object in the data cache. - Args: - data (Any): a data object (DataFeame, GeoJson...) + Parameters + ---------- + data : Any + a data object (DataFeame, GeoJson...) - Returns: - Optional[int]: index of the data object in the cache, or None if absent. + Returns + ------- + int, optional + index of the data object in the cache, or None if absent. """ index = [i for i, d in enumerate(self.data) if d is data] if len(index) == 1: @@ -63,13 +70,18 @@ def cache_index(self, data: Any) -> Optional[int]: return None def merge_default(self, spec: dict) -> dict: - """Merge SpecParser default spec values with an actual spec. + """ + Merge SpecParser default spec values with an actual spec. - Args: - spec (dict): spec to update with default values. + Parameters + ---------- + spec : dict + spec to update with default values. - Returns: - dict: merged spec. + Returns + ------- + dict + merged spec. """ default = self._default for k in default: @@ -78,13 +90,18 @@ def merge_default(self, spec: dict) -> dict: return spec def parse_spec(self) -> dict: - """Start spec parsing from _spec attribute. + """ + Start spec parsing from _spec attribute. - Args: - default (dict): default spec values defined during Creator creation. + Parameters + ---------- + default : dict + default spec values defined during Creator creation. - Returns: - dict: parsed specification. + Returns + ------- + dict + parsed specification. """ # Deep copy should not be needed and copy should be sufficient as # merge_default only affects top-level elements. @@ -93,14 +110,19 @@ def parse_spec(self) -> dict: return self.parse(spec) def parse(self, spec: Any) -> Any: - """Recursively parse part of a Plot specification to check and convert + """ + Recursively parse part of a Plot specification to check and convert its elements. - Args: - spec (Any): part of a specification. + Parameters + ---------- + spec : Any + part of a specification. - Returns: - Any: parsed part of a specification. + Returns + ------- + Any + parsed part of a specification. """ if spec is None: return None @@ -172,21 +194,29 @@ def parse(self, spec: Any) -> Any: return spec def serialize_data(self) -> list: - """Serialize data in the data cache. + """ + Serialize data in the data cache. - Returns: - list: list of serialized data objects. + Returns + ------- + list + list of serialized data objects. """ return [serialize(d, renderer=self.renderer) for d in self.data] def js(txt: str) -> dict: - """Tag a string as JavaScript code. + """ + Tag a string as JavaScript code. - Args: - txt (str): string containing JavaScript code. + Parameters + ---------- + txt : str + string containing JavaScript code. - Returns: - dict: tagged string as dict with type value. + Returns + ------- + dict + tagged string as dict with type value. """ return {"pyobsplot-type": "js", "value": txt} diff --git a/src/pyobsplot/widget.py b/src/pyobsplot/widget.py index c64130ed..4a88f554 100644 --- a/src/pyobsplot/widget.py +++ b/src/pyobsplot/widget.py @@ -2,8 +2,7 @@ Obsplot widget handling. """ - -from typing import Optional +from typing import Any import anywidget import traitlets @@ -13,15 +12,6 @@ class ObsplotWidget(anywidget.AnyWidget): - """Obsplot widget class. - - It inherits from anywidget.Anywidget. - - The class takes a plot specification as input and generates a plot. - - The specification can be given as a dict, a Plot function call or as - Python kwargs. - """ # Disable _esm and _css watching and live reload to avoid "exception not rethrown" # error with pytest. @@ -36,12 +26,26 @@ class ObsplotWidget(anywidget.AnyWidget): def __init__( self, - spec, + *, + spec: Any, theme: str = DEFAULT_THEME, - default: Optional[dict] = None, - debug: bool = False, # noqa: FBT001, FBT002 - ): - """Obsplot widget constructor.""" + default: dict | None = None, + debug: bool = False, + ) -> None: + """ + Obsplot widget class, inherits from anywidget.Anywidget. + + Parameters + ---------- + spec : Any + Plot specification as dict, Plot function call or Python kwargs. + theme : {'light', 'dark', 'current'}, optional + color theme to use, by default 'light' + default : dict, optional + dict of default spec values, by default None + debug : bool, optional + activate debug mode, by default False + """ self._debug = debug self._default = default self._theme = theme