From 2f3a7e2d2697978b925fda8e35d834ca5b59e443 Mon Sep 17 00:00:00 2001 From: FBruzzesi Date: Mon, 15 Jul 2024 20:42:33 +0200 Subject: [PATCH] docs: api-completeness --- utils/api-completeness.md.jinja | 14 ++++ utils/generate_backend_completeness.py | 109 +++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 utils/api-completeness.md.jinja create mode 100644 utils/generate_backend_completeness.py diff --git a/utils/api-completeness.md.jinja b/utils/api-completeness.md.jinja new file mode 100644 index 000000000..9d284db62 --- /dev/null +++ b/utils/api-completeness.md.jinja @@ -0,0 +1,14 @@ +# API Completeness + +Narwhals has two different level of support for libraries: "full" and "interchange". + +Libraries for which we have full support we intend to support the whole Narwhals API, however this is a work in progress. + +In the following table it is possible to check which method is implemented for which backend. + +!!! info + + - "pandas-like" means pandas, cuDF and Modin + - Polars supports all the methods (by design) + +{{ backend_table }} \ No newline at end of file diff --git a/utils/generate_backend_completeness.py b/utils/generate_backend_completeness.py new file mode 100644 index 000000000..2a7dab592 --- /dev/null +++ b/utils/generate_backend_completeness.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import importlib +import inspect +from pathlib import Path +from typing import Any +from typing import Final + +import polars as pl +from jinja2 import Template + +TEMPLATE_PATH: Final[Path] = Path("utils") / "api-completeness.md.jinja" +DESTINATION_PATH: Final[Path] = Path("docs") / "api-reference" / "api-completeness.md" + + +MODULES = ["dataframe", "series", "expression"] +EXCLUDE_CLASSES = {"BaseFrame"} + + +def get_class_methods(kls: type[Any]) -> list[str]: + return [m[0] for m in inspect.getmembers(kls) if not m[0].startswith("_")] + + +def get_backend_completeness_table() -> str: + results = [] + + for module_name in MODULES: + nw_namespace = f"narwhals.{module_name}" + sub_module_name = module_name if module_name != "expression" else "expr" + + narwhals_module_ = importlib.import_module(nw_namespace) + classes_ = inspect.getmembers( + narwhals_module_, + predicate=lambda c: inspect.isclass(c) and c.__module__ == nw_namespace, # noqa: B023, not imported classes + ) + + for nw_class_name, nw_class in classes_: + if nw_class_name in EXCLUDE_CLASSES: + continue + + if nw_class_name == "LazyFrame": + backend_class_name = "DataFrame" + else: + backend_class_name = nw_class_name + + arrow_class_name = f"Arrow{backend_class_name}" + arrow_module_ = importlib.import_module(f"narwhals._arrow.{sub_module_name}") + arrow_class = inspect.getmembers( + arrow_module_, + predicate=lambda c: inspect.isclass(c) and c.__name__ == arrow_class_name, # noqa: B023 + ) + + pandas_class_name = f"PandasLike{backend_class_name}" + pandas_module_ = importlib.import_module( + f"narwhals._pandas_like.{sub_module_name}" + ) + pandas_class = inspect.getmembers( + pandas_module_, + predicate=lambda c: inspect.isclass(c) + and c.__name__ == pandas_class_name, # noqa: B023 + ) + + nw_methods = get_class_methods(nw_class) + arrow_methods = get_class_methods(arrow_class[0][1]) if arrow_class else [] + pandas_methods = get_class_methods(pandas_class[0][1]) if pandas_class else [] + + narhwals = pl.DataFrame( + {"class": nw_class_name, "backend": "narwhals", "method": nw_methods} + ) + arrow = pl.DataFrame( + {"class": nw_class_name, "backend": "arrow", "method": arrow_methods} + ) + pandas = pl.DataFrame( + { + "class": nw_class_name, + "backend": "pandas-like", + "method": pandas_methods, + } + ) + + results.extend([narhwals, pandas, arrow]) + + results = ( + pl.concat(results) # noqa: PD010 + .with_columns(supported=pl.lit(":white_check_mark:")) + .pivot(on="backend", values="supported", index=["class", "method"]) + .filter(pl.col("narwhals").is_not_null()) + .drop("narwhals") + .fill_null(":x:") + .sort("class", "method") + ) + + with pl.Config( + tbl_formatting="ASCII_MARKDOWN", + tbl_hide_column_data_types=True, + tbl_hide_dataframe_shape=True, + set_tbl_rows=results.shape[0], + ): + return str(results) + + +if __name__ == "__main__": + backend_table = get_backend_completeness_table() + + with TEMPLATE_PATH.open(mode="r") as stream: + template = Template(stream.read()).render({"backend_table": backend_table}) + + with DESTINATION_PATH.open(mode="w") as destination: + destination.write(template)